Go语言协程 | 青训营

123 阅读6分钟

Go语言中的协程(Goroutine)是一种轻量级的线程管理机制,用于实现并发和并行编程。

协程和传统的线程相比

与传统的线程(Thread)相比,协程具有以下特点:

  1. 轻量级:协程的创建和销毁成本很低,可以创建大量的协程而不会消耗过多的系统资源。相比之下,传统线程的创建和销毁开销较大。
  2. 原生支持:Go语言内置了对协程的支持,通过关键字go可以启动一个新的协程,无需使用额外的库或框架。
  3. 并发通信:协程之间可以通过通道(Channel)进行高效的通信和数据共享。通道是一种特殊的数据结构,用于在协程之间传递数据,实现数据同步和消息传递。
  4. 调度器管理:Go语言的运行时(Runtime)带有一个称为"调度器"的组件,它负责在多个协程之间进行调度和管理。调度器会自动将协程的执行时间划分为多个时间片(GOMAXPROCS),并在运行时动态调整协程的数量和调度策略,以充分利用系统资源。
  5. 高并发性能:由于协程的轻量级和调度器的优化,Go语言可以轻松创建大量的协程,并发执行它们,从而实现高并发的编程模型。这使得编写高性能的并发程序变得更加容易。

使用协程可以使程序的并发性更高,更容易编写和维护并发代码。通过利用多个协程并发执行任务,可以提高程序的性能和响应性,同时避免了传统线程所带来的复杂性和资源消耗。

GMP模型

GMP(Goroutine, M, P)模型是Go语言运行时(Runtime)中用于实现协程调度和并发执行的模型。它是Go语言并发编程的核心组成部分,负责管理和调度协程(Goroutine)的执行。

  1. 全局队列(Global Queue):存放等待运行的G。
  2. P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
  3. P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。
  4. 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模型的典型生命周期:

  1. 启动:
    • 当程序运行时,Go语言运行时(Runtime)会初始化一些全局状态,包括调度器(Scheduler)和全局队列等。
    • 运行时会创建一个主M(Main M),它与主线程关联,并负责执行程序的入口函数(例如main()函数)。
    • 主M会创建一个主P(Main P),它作为调度器的初始处理器。
  1. 协程创建:
    • 当使用关键字go创建一个新的协程(Goroutine)时,调度器会将该协程放入待执行的Goroutine队列中。
    • 如果存在空闲的P,调度器会将一个Goroutine分配给该P,并将其绑定到一个空闲的M上。
    • 如果没有空闲的P,则调度器会将Goroutine放入全局队列等待分配。
  1. 协程调度:
    • 空闲的M会从待执行的Goroutine队列中获取一个Goroutine,并开始执行它的代码。
    • 当Goroutine发生阻塞(如等待I/O操作、通道操作等)或时间片用完时,M会释放执行权,将Goroutine放回待执行的队列中。
    • 调度器会选择一个新的Goroutine分配给该M,或者将M设置为休眠状态。
  1. 协程销毁:
    • 当一个Goroutine执行完毕(到达代码结尾)或发生异常时,相应的M会退出。
    • 当M退出时,它会释放与之关联的资源,包括内存和堆栈等。
    • 如果某个M长时间没有执行Goroutine(空闲状态),调度器可能会将其停用或销毁,以节省资源。
  1. 关闭:
    • 当程序执行完毕或调用os.Exit()等终止程序的函数时,调度器会关闭,并清理所有相关的资源。
    • 所有剩余的未执行完的Goroutine会被丢弃,不再执行。

通过GMP模型,Go语言能够高效地管理协程的调度和执行,实现并发和并行编程。调度器动态地分配和回收M和Goroutine,使得Go程序能够充分利用多核处理器的性能,实现高效的并发操作。同时,GMP模型的设计使得编写并发代码变得更加简单和高效。