>

goroutine

1)轻量

  • 内存小:2KB 的栈内存空间。作为对比:创建一个 POSIX 线程需要 2MB 的内存空间
  • 用户层:创建goroutine 则是纯用户层的操作。作为对比:创建一个POSIX-thread是一次系统调用,需要陷入内核层,申请OS资源成功后返回用户层;

2)调度复杂度 O(1)
不会随着 goroutine 的增加

3)网络 IO 操作不会阻塞其他 goroutine 的调度
runtime 底层采用 epoll 监听网络 IO 事件。从操作系统角度来看,可以将 go runtime 认为是事件驱动的 C 程序。

Goroutine 调度

G-P-M

  • G: 表示Goroutine,每个Goroutine对应一个G结构体,G存储Goroutine的运行堆栈、状态以及任务函数,可重用。G并非执行体,每个G需要绑定到P才能被调度执行。
  • P: Processor,表示逻辑处理器, 对G来说,P相当于CPU核,G只有绑定到P(在P的local runq中)才能被调度。对M来说,P提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P的数量决定了系统内最大可并行的G的数量(前提:物理CPU核数 >= P的数量),P的数量由用户设置的GOMAXPROCS决定,但是不论GOMAXPROCS设置为多大,P的数量最大为256。
  • M: Machine,OS线程抽象,代表着真正执行计算的资源,在绑定有效的P后,进入schedule循环;而schedule循环的机制大致是从Global队列、P的Local队列以及wait队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到M,如此反复。M并不保留G状态,这是G可以跨M调度的基础,M的数量是不定的,由Go Runtime调整,为了防止创建过多OS线程导致系统调度不过来,目前默认最大限制为10000个。

在Go 1.0发布的时候,它的调度器其实G-M模型,也就是没有P的,调度过程全由G和M完成,这个模型暴露出一些问题:

  • 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有 goroutine 相关操作,比如:创建、重新调度等都要上锁;
  • goroutine 传递问题:M经常在M之间传递『可运行』的 goroutine,这导致调度延迟增大以及额外的性能损耗;
  • 每个 M 做内存缓存,导致内存占用过高,数据局部性较差;
  • 由于 syscall 调用而形成的剧烈的 worker thread 阻塞和解除阻塞,导致额外的性能损耗。

这些问题实在太扎眼了,导致Go1.0虽然号称原生支持并发,但是这些性能问题饱受诟病,核心开发大佬 Dmitry Vyukov 重新设计并实现了现在的 Go 调度器(在原 G-M 模型下引入 P),并且实现了一个 working-steal 的调度算法:

  • 每个P维护一个G的本地队列;
  • 当一个G被创建出来,或者变为可执行状态时,就把他放到P的可执行队列中;
  • 当一个G在M里执行结束后,P会从队列中把该G取出;如果此时P的队列为空,即没有其他G可以执行, M就随机选择另外一个P,从其可执行的G队列中取走一半。

该算法避免了在goroutine调度时使用全局锁。

至此,Go调度器的基本模型确立:

参考:
https://juejin.im/entry/5b2878c7f265da5977596ae2

1、协作式
Go 在设计之初并没考虑将 goroutine 设计成抢占式的。用户负责让各个 goroutine 交互合作完成任务。一个 goroutine 只有在涉及到加锁,读写通道或者主动让出 CPU 等操作时才会触发切换。

垃圾回收器是需要stop the world的。如果垃圾回收器想要运行了,那么它必须先通知其它的goroutine合作停下来,这会造成较长时间的等待时间。考虑一种很极端的情况,所有的goroutine都停下来了,只有其中一个没有停,那么垃圾回收就会一直等待着没有停的那一个。

2、抢占式

Go在1.2版中开始引入比较初级的抢占式调度,当然,只是引入了一些很初级的抢占,并没有像操作系统调度那么复杂,没有对goroutine分时间片,设置优先级等。
只有长时间阻塞于系统调用,或者运行了较长时间才会被抢占。runtime会在后台有一个检测线程(sysmon),它会检测这些情况,并通知goroutine执行调度。

sysmon

rutime sysmon 不绑定到任何 P 就可以运行,并且其会定期唤醒作系统状态检查。运行一个sysmon函数。这个函数会周期性地做epoll操作,同时它还会检测每个P是否运行了较长时间。
如果检测到某个P状态处于Psyscall超过了一个sysmon的时间周期(20us),并且还有其它可运行的任务,则切换P。
如果检测到某个P的状态为Prunning,并且它已经运行了超过10ms,则会将P的当前的G的stackguard设置为StackPreempt。这个操作其实是相当于加上一个标记,通知这个G在合适时机进行调度。
目前这里只是尽最大努力送达,但并不保证收到消息的goroutine一定会执行调度让出运行权。

GC