一. MPG模式
Go语言的调度模型被称为GMP,这是一个高效且复杂的调度系统,用于在可用的物理线程上调度goroutines(Go的轻量级线程)。GMP模型由三个主要组件构成:Goroutine、M(机器)和P(处理器)。下面详细介绍这三个组件以及它们如何协同工作。
1. Goroutine(G)
Goroutine 是Go语言中的一个基本概念,类似于线程,但比线程更轻量。Goroutines在Go的运行时环境中被调度和管理,而非操作系统。Goroutines非常轻量,启动快,且切换开销小。这是因为它们有自己的栈,这个栈可以根据需要动态增长和缩减。
2. Machine(M)
M 代表了真正的操作系统线程。每个M都由操作系统调度,并且拥有一个固定大小的内存栈用于执行C代码。M负责执行Goroutines的代码。Go的运行时会尽量复用M,以减少线程的创建和销毁带来的开销。
3. Processor(P)
P 是Go运行时的一个资源,可以看作是执行Goroutines所需的上下文环境。P的数量决定了系统同时运行Goroutines的最大数量。每个P都有一个本地的运行队列,用于存放待运行的Goroutines。P的数量一般设置为等于机器的逻辑处理器数量,以充分利用多核的优势。
MPG 工作方式
在程序启动时,Go运行时会根据可用的核心数创建一定数量的P。每个P都会与一个M绑定在一起,这个M会从P的本地运行队列中取出一个G来执行。当Goroutine阻塞时(比如等待I/O),执行它的M会被解绑,并且该Goroutine会被移动到全局队列或者等待队列中,让其他M可以接管这个P并继续执行其他Goroutines。如果所有的M都阻塞了,运行时会创建额外的M来保证至少有一个M是非阻塞的,以继续执行Goroutines。
调度优势
Go调度器的设计使得成千上万的Goroutines能够在数量较少的线程(M)上高效运行,这极大地减少了上下文切换的开销。Go的调度器是协作式的,这意味着Goroutines需要自己释放控制权。通常,这发生在显式的阻塞操作(如I/O操作)或者隐式的调度点(如函数调用)时。
二.互斥锁实现阶乘计算
1.代码
package main
import (
"fmt"
"sync"
)
// 多协程计算阶乘
var (
myMap = make(map[int]int) // 全局变量
mu sync.Mutex // 安全访问myMap
wg sync.WaitGroup // 等待所有协程完成
)
func main() {
// 开启协程
for i := 1; i <= 10; i++ {
wg.Add(1)
go factorial(i)
}
// 等待所有协程完成后再打印
wg.Wait()
// 遍历myMap并打印结果
for i, v := range myMap {
fmt.Println(i, v)
}
}
// 计算阶乘
func factorial(n int) {
res := 1
// 计算阶乘
for i := 1; i <= n; i++ {
res *= i
}
// 递延地减少WaitGroup计数器
defer wg.Done()
// 在修改myMap之前进行互斥锁操作
mu.Lock()
myMap[n] = res
mu.Unlock()
}
2.MPG模型解释
Go语言的并发模型被称为MPG模型,其中:
M 代表机器(Machine),是对操作系统线程的抽象。P 代表处理器(Processor),是对M进行调度的上下文。G 代表Goroutine,是Go的轻量级线程,它在M上执行。
当一个Go程序运行时:
Goroutines(G) 在**Processors(P)**上被调度。每个P都绑定到一个M(操作系统线程),但一个P可以调度多个G。在这段代码中,当main函数启动多个goroutines时,这些G被分配到不同的P上,并且可能在不同的M上执行。当一个G在执行阶乘计算时,如果它需要等待(例如,等待互斥锁),它会被P挂起,并且P会转而执行另一个G。一旦所有的G都执行完毕(即wg.Wait()返回),程序进入最后阶段,遍历并打印myMap中存储的结果。
3.管道channel基本概念
创建管道
使用make关键字创建管道。可以创建有缓冲的管道或无缓冲的管道。示例:ch := make(chan int) 创建一个传递整型数据的无缓冲管道。无缓冲管道:这种管道没有存储空间,因此发送操作(ch <- v)会阻塞,直到另一端有goroutine准备好接收(<-ch)。无缓冲管道确保同时只有一个数据在通道中传递,它强制发送者和接收者同步交换数据。有缓冲管道:这种管道有一个指定的容量,允许在接收者准备好接收之前存储有限数量的值。如果管道满了(即达到其容量限制),发送操作将阻塞;如果管道为空,接收操作将阻塞。有缓冲管道提供了一定程度的松耦合,允许发送者和接收者在缓冲区不满和不空的情况下独立操作。
发送和接收数据
使用箭头操作符(<-)来发送和接收数据。示例:ch <- v 表示将值v发送到管道ch,v := <-ch 表示从管道ch接收值并赋给变量v。
无缓冲与有缓冲
无缓冲管道:发送操作会阻塞,直到另一端有goroutine进行接收操作。有缓冲管道:只有当缓冲区满时发送操作才会阻塞,只有当缓冲区空时接收操作才会阻塞。
使用场景
同步:管道可用于不同goroutines之间的同步。数据共享:通过管道安全地在goroutines之间传递数据,防止竞争条件。流程控制:通过有缓冲管道控制处理速度和压力。
重要特性
安全性:管道在内部实现了必要的同步机制,因此在多个goroutines访问时是安全的。 阻塞性:无缓冲管道在发送或接收时会阻塞,直到另一端准备好。 关闭管道: 使用close函数关闭管道。 关闭管道后,不能再向管道发送数据,但仍可以接收管道中已存在的数据。 尝试向已关闭的管道发送数据会引发panic。 范围循环:可以使用for range循环从管道接收数据,直到管道被关闭。
注意事项
死锁:如果不正确使用管道,特别是在管道操作之间没有适当的同步时,可能导致死锁。资源管理:应确保在不再需要时关闭管道,以避免内存泄漏。
4.管道channel实现阶乘计算
package main
import (
"fmt"
"sync"
)
var (
ch = make(chan int) // FIFO 队列 first in first out 线程安全
wg2 sync.WaitGroup // 用于等待所有goroutine完成
)
func main() {
for i := 1; i <= 10; i++ {
// 添加WaitGroup的计数
wg2.Add(1)
go calChannel(i)
}
wg2.Wait() // 等待所有goroutine完成
close(ch) // 关闭通道
// 启动一个新的goroutine来打印管道中的值
go func() {
for v := range ch {
fmt.Println(v)
}
}()
}
func calChannel(n int) {
defer wg2.Done() // 在函数退出时通知WaitGroup
// 通过通道计算阶乘
res := 1
for i := 1; i <= n; i++ {
res *= i
}
ch <- res
}
三. interface{}类型
package main
import "fmt"
func main() {
ch := make(chan interface{}, 3)
ch <- 88
ch <- "i am god"
cat := Cat{Name: "小花猫", Age: 4}
ch <- cat
close(ch)
// 丢弃管道中的值
<-ch
<-ch
v := <-ch
fmt.Printf("%T", v)
fmt.Println()
// 需要类型断言 ∵从管道中取出的值类型实际是interface{}类型 只有空接口类型才可以类型断言
val := v.(Cat)
fmt.Printf("%v", val.Name)
}
type Cat struct {
Name string
Age int
}
相关阅读
发表评论