读 Go 调度系列的笔记
Golang 自己维护线程池来对接 Goroutine,降低系统的调度成本。
程序性能差的原因 #
- 线程处于 Waiting 状态。由于等待硬件(disk, network)操作系统(system calls)同步调用(atomic, mutexes)导致线程处于 Waiting 状态。
- Context Switching。大量的线程处在 Runnable 状态争抢时间片导致的 Context Switching。
- 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
允许调度器做出调度决策的事件 #
- go 关键字
- Garbage collection
- System calls
- Synchronization and Orchestration
如何调度来保证程序性能 #
协作式调度(Cooperating Scheduler)
一, 通过轮询器进行异步系统调用 #
- 通过使用网络轮询器(network poller)进行网络系统调用,调度程序可以防止 Goroutine 在进行这些系统调用时阻止 M。 这有助于使 M 保持可用以执行 P 的 LRQ 中的其他 Goroutine,而无需创建新的 M。这有助于减少 OS 上的调度负载。
- 其次把该 Goroutine 移回 该 P 的 LRQ 中。
二, 通过分离 M 和 P 进行同步系统调用 #
- 调度程序能够识别 Goroutine-1 导致 M 阻塞。 此时,调度程序将 M1 与 P 分离,而阻塞 Goroutine-1 仍然处于连接状态。 然后,调度程序引入一个新的 M2 来为 P 服务。此时,可以从 LRQ 中选择 Goroutine-2,并在 M2 上进行上下文切换。
- Goroutine-1 完成了阻塞系统调用。此时,Goroutine-1 可以移回 LRQ 并再次由 P 提供服务。将 M1 放在一边以备将来使用。
三, 窃取工作(Work Stealing) #
- P1 没有更多的 Goroutines 要执行,P1 需要检查 P2 在其 LRQ 中是否有 Goroutines,并取它找到的一半。
- P2 完成了所有工作。首先,它将查看 P1 的 LRQ,但找不到任何 Goroutine。 接下来,将查看 GRQ。
编写并发程序需要考虑的问题 #
- 确定工作负载是否适合并发
- 确定必须使用正确语义的工作负载类型(CPU-Bound / IO-Bound)非常重要。
References #
Scheduling In Go : Part I - OS Scheduler | Archive
Scheduling In Go : Part II - Go Scheduler | Archive
Scheduling In Go : Part III - Concurrency | Archive
- Previous: Hello World
- Next: 浏览器打开 Google.com 中涉及的 DNS 查找