注意,原文发布日期为2017年5月3日,至今已有3年 , 而 go 语言已然从 1.10 进化到了 如今的 1.14 , 所以本篇的内容时效性并不做保证,仅用以学习理解。
基础
Go 的 runtime
调度器把 goroutines
映射为操作系统的线程,所以实质上, goroutines
是轻量化版本的,可以以极低的开销运作的线程。 所以 gotoutines
既是 G.M.P
模型里面的 G
。runtime
会追踪每一个 G
,并把他们映射到每一个逻辑处理器 (Logical Processors
) , 即 P
。P
可以看作是一种需要被获取到的抽象的资源或者上下文,以使系统的线程 (OS thread
) 即 M
(Machine
) 可以执行 G
。
你可以通过 runtime.GOMAXPROCS()
来控制讲要使用的 P
的数量 。注意,这条命令尽量只执行一次,因为它会触发 GC
,并导致 STW
。
事实上,操作系统运行线程,也就是你的代码所运行的地方。 Go
做的 “把戏” 就是,使用编译器把不同系统的系统调用注入 go 的 runtime
内, 所以 Go 可以响应调度器并且执行动作。
在 Ms , Ps , Gs 间起舞吧(?
Ms , Ps , Gs 之间的交流有点复杂,噢,上帝,看看这美妙的流程图:
我们可以看到, 这里有两种给 G
用的队列: 一个是实现于 schedt struct ( 极少会被用到的 )的全局队列,以及每一个 P
所维护的一整套 可运行的 G
的队列 。
为了执行 goroutine , M
需要维持一个 context
(上下文) P
。然后M
从 P
队列中推出 goroutines
,并执行代码。
当你调度了一个新的 goroutine (执行一个go func()
),意味着你把它压入了 P
的队列,他们有一个很有趣的 偷工作
调度算法,当 M
完成执行了一些 G
后 ,它就会设法从队列中取出其他 G
, 当其中一个 P
的队列空了之后 , G
会从其他 P
中尝试 “偷” 掉一半的可执行的 G
!
当你的 goroutine
做了一个阻塞性的系统调用(syscall)时,有趣的事情就发生了。 阻塞性的系统调用会被拦截, 如果当时有其他的 G
要执行时,runtime
会把这个线程从 P
中分离 ,并创建一个新的 OS 线程 ( 当闲置线程不存在时 ) ,来服务这个处理器。
当系统调用回复时,此 goroutine
会放回本地可执行队列,并且线程会自动释放(*此处用了park ,即停车,应该可以理解为释放为闲置资源。)(意味着线程不再使用) , 并把自己写入闲置线程列表。
如果一个 goroutine
执行了一个网络调用,runtime
会做一相似的动作。这个调用会被拦截,但因为 Go 有一套拥有自己线程的网络池 , 它们会被分配到这些任务。
本质上,当目前的gooutine
被阻塞在:
- 阻塞的系统调用(比如打开一个文件),
- 网络输入,
- go channel 操作,
- 原生的
sync
包调用。
时,Go 的 runtime
会执行另外的 goroutine
。