Go语言中的协程(Goroutine)是一种轻量级的线程管理机制,用于实现并发和并行编程。
协程和传统的线程相比
与传统的线程(Thread)相比,协程具有以下特点:
- 轻量级:协程的创建和销毁成本很低,可以创建大量的协程而不会消耗过多的系统资源。相比之下,传统线程的创建和销毁开销较大。
- 原生支持:Go语言内置了对协程的支持,通过关键字
go可以启动一个新的协程,无需使用额外的库或框架。 - 并发通信:协程之间可以通过通道(Channel)进行高效的通信和数据共享。通道是一种特殊的数据结构,用于在协程之间传递数据,实现数据同步和消息传递。
- 调度器管理:Go语言的运行时(Runtime)带有一个称为"调度器"的组件,它负责在多个协程之间进行调度和管理。调度器会自动将协程的执行时间划分为多个时间片(GOMAXPROCS),并在运行时动态调整协程的数量和调度策略,以充分利用系统资源。
- 高并发性能:由于协程的轻量级和调度器的优化,Go语言可以轻松创建大量的协程,并发执行它们,从而实现高并发的编程模型。这使得编写高性能的并发程序变得更加容易。
使用协程可以使程序的并发性更高,更容易编写和维护并发代码。通过利用多个协程并发执行任务,可以提高程序的性能和响应性,同时避免了传统线程所带来的复杂性和资源消耗。
GMP模型
GMP(Goroutine, M, P)模型是Go语言运行时(Runtime)中用于实现协程调度和并发执行的模型。它是Go语言并发编程的核心组成部分,负责管理和调度协程(Goroutine)的执行。
- 全局队列(Global Queue):存放等待运行的G。
- P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
- P列表:所有的P都在程序启动时创建,并保存在数组中,最多有
GOMAXPROCS(可配置)个。 - M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。
调度器的设计策略
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
1)work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
2)hand off机制
当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。
利用并行:GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行。GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。
抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。
全局G队列:在新的调度器中依然有全局G队列,当P的本地队列为空时,优先从全局队列获取,如果全局队列为空时则通过work stealing机制从其他P的本地队列偷取G。
GMP模型生命周期
GMP(Goroutine, M, P)模型在Go语言运行时中有一个生命周期,涵盖了协程的创建、调度和销毁等过程。下面是GMP模型的典型生命周期:
- 启动:
-
- 当程序运行时,Go语言运行时(Runtime)会初始化一些全局状态,包括调度器(Scheduler)和全局队列等。
- 运行时会创建一个主M(Main M),它与主线程关联,并负责执行程序的入口函数(例如
main()函数)。 - 主M会创建一个主P(Main P),它作为调度器的初始处理器。
- 协程创建:
-
- 当使用关键字
go创建一个新的协程(Goroutine)时,调度器会将该协程放入待执行的Goroutine队列中。 - 如果存在空闲的P,调度器会将一个Goroutine分配给该P,并将其绑定到一个空闲的M上。
- 如果没有空闲的P,则调度器会将Goroutine放入全局队列等待分配。
- 当使用关键字
- 协程调度:
-
- 空闲的M会从待执行的Goroutine队列中获取一个Goroutine,并开始执行它的代码。
- 当Goroutine发生阻塞(如等待I/O操作、通道操作等)或时间片用完时,M会释放执行权,将Goroutine放回待执行的队列中。
- 调度器会选择一个新的Goroutine分配给该M,或者将M设置为休眠状态。
- 协程销毁:
-
- 当一个Goroutine执行完毕(到达代码结尾)或发生异常时,相应的M会退出。
- 当M退出时,它会释放与之关联的资源,包括内存和堆栈等。
- 如果某个M长时间没有执行Goroutine(空闲状态),调度器可能会将其停用或销毁,以节省资源。
- 关闭:
-
- 当程序执行完毕或调用
os.Exit()等终止程序的函数时,调度器会关闭,并清理所有相关的资源。 - 所有剩余的未执行完的Goroutine会被丢弃,不再执行。
- 当程序执行完毕或调用
通过GMP模型,Go语言能够高效地管理协程的调度和执行,实现并发和并行编程。调度器动态地分配和回收M和Goroutine,使得Go程序能够充分利用多核处理器的性能,实现高效的并发操作。同时,GMP模型的设计使得编写并发代码变得更加简单和高效。