读 Go 调度系列的笔记

Golang 自己维护线程池来对接 Goroutine,降低系统的调度成本。

程序性能差的原因

  1. 线程处于 Waiting 状态。由于等待硬件(disk, network)操作系统(system calls)同步调用(atomic, mutexes)导致线程处于 Waiting 状态。
  2. Context Switching。大量的线程处在 Runnable 状态争抢时间片导致的 Context Switching。
  3. Cache-Coherency problem。由于每个核心都有 Cache Lines 的副本(3~40个时钟周期),并行运行的多个线程读写相同或相邻的 Cache Lines 数据时,会导致脏页的产生,进而导致其他线程读写时需要去主存储器访问(100〜300个时钟周期)才能获取缓存行的新副本。

调度名词

P(Processor) logical processors

M(Machine) OS Thread

G(Goroutine) application-level threads

GRQ Global Run Queue

LRQ Local Run Queue

允许调度器做出调度决策的事件

  1. go 关键字
  2. Garbage collection
  3. System calls
  4. Synchronization and Orchestration

如何调度来保证程序性能

协作式调度(Cooperating Scheduler)

一, 通过轮询器进行异步系统调用

  1. 通过使用网络轮询器(network poller)进行网络系统调用,调度程序可以防止 Goroutine 在进行这些系统调用时阻止 M。 这有助于使 M 保持可用以执行 P 的 LRQ 中的其他 Goroutine,而无需创建新的 M。这有助于减少 OS 上的调度负载。
  2. 其次把该 Goroutine 移回 该 P 的 LRQ 中。

二, 通过分离 M 和 P 进行同步系统调用

  1. 调度程序能够识别 Goroutine-1 导致 M 阻塞。 此时,调度程序将 M1 与 P 分离,而阻塞 Goroutine-1 仍然处于连接状态。 然后,调度程序引入一个新的 M2 来为 P 服务。此时,可以从 LRQ 中选择 Goroutine-2,并在 M2 上进行上下文切换。
  2. Goroutine-1 完成了阻塞系统调用。此时,Goroutine-1 可以移回 LRQ 并再次由 P 提供服务。将 M1 放在一边以备将来使用。

三, 窃取工作(Work Stealing)

  1. P1 没有更多的 Goroutines 要执行,P1 需要检查 P2 在其 LRQ 中是否有 Goroutines,并取它找到的一半。
  2. P2 完成了所有工作。首先,它将查看 P1 的 LRQ,但找不到任何 Goroutine。 接下来,将查看 GRQ。

编写并发程序需要考虑的问题

  1. 确定工作负载是否适合并发
  2. 确定必须使用正确语义的工作负载类型(CPU-Bound / IO-Bound)非常重要。

References

Scheduling In Go : Part I - OS SchedulerArchive

Scheduling In Go : Part II - Go SchedulerArchive

Scheduling In Go : Part III - ConcurrencyArchive

All rights reserved
Except where otherwise noted, content on this page is copyrighted.