goroutine在go里面是叫协程的,一般情况下进程里处理并发任务时都会开启多个线程来处理,线程的生命周期是通过操作系统控制的,而协程就相当于go在线程的基础上,又往下写了一套线程的架构,然后不给操作系统管理,自己写一套调度策略来使用,有进程-线程-协程这种关系。
协程的话,可以算做一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
在go中每一个协程是一个独立的执行单元,相较于每个线程固定分配 2M 内存的模式,协程的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达 1GB(64 位机器最大是 1G,32 位机器最大是 256M),且完全由 go自己的调度器来调度。此外,go的gc还会周期性地将不再使用的内存回收,收缩栈空间,因此,go程序可以同时并发成千上万个 协程是得益于它强劲的调度器和高效的内存模型。
下面详细讲解协程调度的细节。
协程调度通过 GPM 模型+其他协作的数据模型实现。
goroutine, 代表一个协程,包含自己的栈、程序计数器等信息。
machine,代表线程,一个 M 对应一个线程。
M并没有像G和P一样的状态标记, 但可以认为一个M有以下的状态:
自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量。
processor,指处理器,主要用途是用来执行协程,维护一个全局运行队列和gFree空闲协程列表。
P提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量默认为cpu核数,也可由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。GOMAXPROCS可通过2种方式设置:
调度器,专门调度协程执行。维护一个全局运行队列和gFree空闲协程列表。
在go中有多个运行队列可以保存待运行(_Grunnable)的G, 它们分别是各个P中的本地运行队列和全局运行队列。
入队待运行的G时会优先加到当前P的本地运行队列, M获取待运行的G时也会优先从拥有的P的本地运行队列获取,本地运行队列入队和出队不需要使用线程锁。
本地运行队列有数量限制, 当数量达到256个时会入队到全局运行队列。
本地运行队列的数据结构是环形队列 由一个256长度的数组和两个序号(head, tail)组成。
当M从P的本地运行队列获取G时, 如果发现本地队列为空会尝试从其他P盗取一半的G过来,这个机制叫做Work
Stealing。
全局运行队列保存在sched中, 全局运行队列入队和出队需要使用线程锁。
全局运行队列的数据结构是链表, 由两个指针(head, tail)组成。
当M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 空闲M链表保存在全局变量sched。
进入休眠的M会等待一个信号量(m.park), 唤醒休眠的M会使用这个信号量。
go需要保证有足够的M可以运行G, 是通过这样的机制实现的:
因为"入队待运行的G"和"M离开自旋状态"会同时进行, go会使用这样的检查顺序:
入队待运行的G => 内存屏障 => 检查当前自旋的M数量 => 唤醒或者新建一个M,减少当前自旋的M数量 => 内存屏障 => 检查所有运行队列是否有待运行的G => 休眠
这样可以保证不会出现待运行的G入队了, 也有空闲的资源P, 但无M去执行的情况。
当P的本地运行队列中的所有G都运行完毕, 又不能从其他地方拿到G时,拥有P的M会释放P并进入休眠状态, 释放的P会变为空闲状态并加到空闲P链表中, 空闲P链表保存在全局变量sched。
下次待运行的G入队时如果发现有空闲的P, 但是又没有自旋中的M时会唤醒或者新建一个M, M会拥有这个P, P会重新变为运行中的状态。
我们在创建协程只需通过关键字go即可,编译器会自动帮我们关联到相应的方法中。下面是通过runtime.newproc方法作为入口总结出的关键步骤:
go程序是通过循环调用runtime.schedule方法去实现调度,关键步骤:
runtime在go程序启动时,会自动创建一个线程,运行sysmon方法。 sysmon方法在整个程序生命周期中一直执行,负责监视各个协程的状态、判断是否要进行垃圾回收等, 默认是每个协程最多只能执行10毫秒便得把执行权交给下一个协程了。
如果说协程是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个协程发送特定值到另一个协程的通信机制。
go语言中的channel是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是协程的上下文,包含协程的运行状态、环境、现场等信息,用途是在于控制协程的生命周期。