Skip to content

协程

  • 是轻量级的执行单元,由 Go 运行时(而非操作系统)管理,是 Go 并发编程的核心特性。

系统运行单元

程序(program)

  • 是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。

进程(process)

  • 进程是操作系统资源分配的基本单位,是一个正在运行的程序实例,拥有独立的内存空间、文件描述符、寄存器状态等系统资源。
  • 进程间相互独立,通信需要通过 IPC(如管道、信号、共享内存等),开销较大。
  • 由操作系统内核调度,切换成本高(需保存 / 恢复整个进程的上下文)。
  • 一个程序可以启动多个进程(如多进程服务),但资源消耗高。

线程(thread)

  • 线程是进程内的执行单元,是操作系统调度的基本单位,共享所属进程的内存空间和资源,但有独立的栈和寄存器。
  • 线程属于进程,一个进程可包含多个线程(多线程),线程间通信通过共享内存,比进程通信高效。
  • 由操作系统内核调度(内核级线程),切换成本中等(需保存 / 恢复线程上下文,但共享进程资源)。
  • 线程数量有限(受系统内存和内核限制),过多线程会导致调度效率下降。

协程(goroutine)

  • 协程是一种用户态的轻量级线程,由 Go 运行时管理,比线程更轻量级,切换成本低。

GMP 调度模型

GMP.png

运行是调度

  • 协程不由操作系统内核调度,而是由 Go 运行时的 GMP 调度模型 管理
    • G:Goroutine 表示一个协程,是 Go 语言并发的基本单元。
    • M:Machine 表示操作系统内核线程(OS Thread),是真正执行代码的 “物理载体”。
    • P:Processor 表示 “逻辑处理器”,是连接 G 和 M 的中间层,负责管理 G 的调度队列。

调度流程

G的创建与入队

  • 当使用 go 关键字创建 G 时,G 会被加入当前 P 的本地运行队列(LRQ)。若 LRQ 满(默认容量 256),则会被转移到全局运行队列(Global Run Queue, GRQ)。

M 绑定 P 执行 G

  • M 启动后,会从 PLRQ中获取G` 并执行。
  • LRQ 为空,P 会先尝试从 GRQ 中 “偷” 一批 G(通常是 GRQ 一半)到 LRQ,再执行。
  • GRQ 也为空,P 会从其他 PLRQ 中 “偷” 取 G(负载均衡,通常偷取对方一半的 G),避免自身 M 空闲。

G 的阻塞与唤醒

  • G 执行阻塞操作(如 channel 通信、time.Sleep、锁等待、系统调用等):
    • 若阻塞在 Go 运行时内部(如 channel 或 sync.Mutex),G 会被标记为阻塞状态,从 PLRQ 移除,M 会继续从 LRQ 中获取其他 G 执行(此时 G 不会阻塞 M)。
    • 若阻塞在系统调用(如网络 I/O、文件读写),M 会暂时与 P 解绑,进入休眠状态;当系统调用完成后,G 会被重新加入某个 PLRQGRQ,等待再次被调度。

时间片轮转

  • 为避免单个 G 长期占用 M,Go 调度器会为 G 设置时间片(默认约 10ms)。当时间片用完,G 会被暂停,重新放入 LRQ 尾部,让其他 G 获得执行机会(实现公平调度)。

GOMAXPROCS

  • GOMAXPROCS 控制 P 的数量,直接决定了程序的最大并行度(同一时间最多有 GOMAXPROCS 个 G 被执行)。
  • 默认值为 CPU 核心数(可通过 runtime.NumCPU() 查看),通常无需修改。