Go1.0 源码

  1. static void
  2. schedule(G *gp)
  3. {
  4. ...
  5. schedlock();
  6. if(gp != nil) {
  7. ...
  8. switch(gp->status){
  9. case Grunnable:
  10. case Gdead:
  11. // Shouldn't have been running!
  12. runtime·throw("bad gp->status in sched");
  13. case Grunning:
  14. gp->status = Grunnable;
  15. gput(gp);
  16. break;
  17. }
  18. gp = nextgandunlock();
  19. gp->readyonstop = 0;
  20. gp->status = Grunning;
  21. m->curg = gp;
  22. gp->m = m;
  23. ...
  24. runtime·gogo(&gp->sched, 0);
  25. }
  • 调用 schedlock 方法来获取全局锁。
  • 获取全局锁成功后,将当前 Goroutine 状态从 Running(正在被调度) 状态修改为 Runnable(可以被调度)状态。
  • 调用 gput 方法来保存当前 Goroutine 的运行状态等信息,以便于后续的使用。
  • 调用 nextgandunlock 方法来寻找下一个可运行 Goroutine,并且释放全局锁给其他调度使用。
  • 获取到下一个待运行的 Goroutine 后,将其运行状态修改为 Running。
  • 调用 runtime·gogo 方法,将刚刚所获取到的下一个待执行的 Goroutine 运行起来,进入下一轮调度。

实现有如下的问题:

  • 存在单一的全局 mutex(Sched.Lock)和集中状态管理:
    • mutex 需要保护所有与 goroutine 相关的操作(创建、完成、重排等),导致锁竞争严重。
  • Goroutine 传递的问题:
    • goroutine(G)交接(G.nextg):工作者线程(M’s)之间会经常交接可运行的 goroutine。
    • 上述可能会导致延迟增加和额外的开销。每个 M 必须能够执行任何可运行的 G,特别是刚刚创建 G 的 M。
  • 每个 M 都需要做内存缓存(M.mcache):
    • 会导致资源消耗过大(每个 mcache 可以吸纳到 2M 的内存缓存和其他缓存),数据局部性差。
  • 频繁的线程阻塞/解阻塞:
    • 在存在 syscalls 的情况下,线程经常被阻塞和解阻塞。这增加了很多额外的性能开销。

加入P之后会带来什么改变 ?

  • 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。
  • 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。