Go 语言并发编程的核心 —— GMP 调度模型
在 Go 语言中,GMP 调度模型是实现并发的重要手段之一。GMP 调度模型的核心思想是将 M(Machine)、G(Goroutine)和 P(Processor)三个概念分离开来,通过调度器来协调它们之间的关系,从而实现高效的并发。
M(Machine)
M 代表着操作系统中的线程,它是 Go 语言中的执行单位。在程序启动时,Go 语言会创建一定数量的 M,每个 M 都会绑定一个 P。M 的数量默认是 CPU 核心数,但是可以通过 GOMAXPROCS 环境变量来设置。
G(Goroutine)
Goroutine 是 Go 语言中的轻量级线程,它可以与 M 一起调度执行。在程序中,我们可以通过关键字 go 来启动一个 Goroutine,例如:
在上面的例子中,我们使用 go 关键字启动了一个 Goroutine,并在其中执行业务逻辑。需要注意的是,Goroutine 是由 Go 语言的运行时(runtime)进行调度的,而不是由操作系统进行调度,因此它具有轻量级、高效等特点。
P(Processor)
Processor 是 Go 语言中的处理器,它负责将 Goroutine 分配给 M 执行。每个 M 都会绑定一个 P,而 P 的数量可以通过 runtime.NumCPU()来获取(不同于 M 的数量)。
调度器
调度器是 GMP 调度模型的核心,它负责将 Goroutine 分配给 M 执行,并在 M 的数量不足时创建新的 M。调度器还可以将 M 从一个 P 转移到另一个 P,以达到负载均衡的目的。
调度器的实现方式比较复杂,但是它的工作原理可以简单概括如下:
当一个 Goroutine 被启动时,它会被放入一个全局的运行队列中(称为全局队列)。
当一个 M 空闲时,它会从全局队列中获取一个 Goroutine,并开始执行它。
当一个 Goroutine 阻塞时,它会被放入一个本地的等待队列中(称为本地队列)。
当一个 M 中的本地队列为空时,它会从全局队列中获取一批 Goroutine,并将它们放入本地队列中。
当一个 P 中的本地队列为空时,它会从其他 P 中的本地队列中获取一批 Goroutine,并将它们放入本地队列中。
当一个 M 执行时间过长时,调度器会中断它的执行,并将它的状态保存到一个全局的挂起队列中。下次该 M 被分配到执行时,它会从挂起队列中恢复状态,并继续执行。
当一个 M 执行的 Goroutine 数量达到一定阈值时,调度器会将它的状态保存到一个全局的休眠队列中。下次该 M 被分配到执行时,它会从休眠队列中恢复状态,并继续执行。
下面我们来看一个简单的示例,它通过启动多个 Goroutine 来计算斐波那契数列的值:
在上面的例子中,我们启动了 10 个 Goroutine,并在其中计算斐波那契数列的值。由于斐波那契数列的计算是 CPU 密集型的,因此这个程序会利用 GMP 调度模型来实现高效的并发。
注意事项
在使用 GMP 调度模型时,需要注意以下几点:
不要在 Goroutine 中阻塞或者进行长时间的计算,这会导致 M 被挂起或者休眠,从而影响程序的性能。
不要在 Goroutine 中访问共享资源时不加锁,这会导致数据竞争,从而引发难以排查的 bug。
不要将过多的 Goroutine 放入全局队列中,这会导致调度器的性能下降,从而影响程序的性能。
不要将过多的 M 创建出来,这会导致系统资源的浪费,从而影响程序的性能。
版权声明: 本文为 InfoQ 作者【Jack】的原创文章。
原文链接:【http://xie.infoq.cn/article/deda6d1fd420842bd5b3e51ff】。文章转载请联系作者。
评论