V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
darluc
V2EX  ›  Go 编程语言

译文: Go: Goroutine 的抢占机制

  •  
  •   darluc · 2020-03-11 01:27:46 +08:00 · 2710 次点击
    这是一个创建于 1505 天前的主题,其中的信息可能已经有所发展或是发生改变。

    查看全文

    翻译自:Go: Goroutine and Preemption

    ℹ︎本文内容基于 Go 1.13 版本。

    Go 语言利用内部的调度器管理 goroutine。这个调度器致力于对 goroutine 进行切换,确保它们都能够获得执行时间。不过,调度器有时会抢占这些 goroutine 的运行时间以保证正确的轮换。

    调度器和抢占机制

    让我们使用一个简单的例子来说明调度器是如何工作的:为了便于阅读,这些例子不会使用原子操作

    func main() {
       var total int
       var wg sync.WaitGroup
    
       for i := 0; i < 10; i++ {
          wg.Add(1)
          go func() {
             for j := 0; j < 1000; j++ {
                total += readNumber()
             }
             wg.Done()
          }()
       }
    
       wg.Wait()
    }
    
    //go:noinline
    func readNumber() int {
       return rand.Intn(10)
    }
    

    下面是追踪信息( tracing ):

    我们可以清楚地观察到调度器控制 goroutine 在处理器上进行轮换,对它们全部给予相应的执行时间。当 goroutine 由于系统调用,channel 阻塞,睡眠,等待互斥量等操作而停止时,Go 会对其进行调度。在上一个例子中,调度器利用了数字生成器中的互斥量,从而给所有 goroutine 执行时间。在追踪信息中也是可以看到的:

    不过,如果 goroutine 自身没有任何的停滞,Go 还是需要有办法停止正在运行的 goroutine。这种行为被称作抢占( preemption ),它允许调度器对 goroutine 的执行进行切换。任何执行时间超过 10 毫秒的 goroutine 会被标记为可抢占。而后,抢占行为会发生在函数调用的开始阶段,goroutine 调用栈增加的时候。

    让我们来看一个例子,它与上个例子的区别在于去除了数字生成器中的锁:

    func main() {
       var total int
       var wg sync.WaitGroup
    
       for i := gen(0); i < 20; i++ {
          wg.Add(1)
          go func(g gen) {
             for j := 0; j < 1e7; j++ {
                total += g.readNumber()
             }
             wg.Done()
          }(i)
       }
    
       wg.Wait()
    }
    
    var generators [20]*rand.Rand
    
    func init() {
       for i := int64(0); i < 20; i++  {
          generators[i] = rand.New(rand.NewSource(i).(rand.Source64))
       }
    }
    
    type gen int
    //go:noinline
    func (g gen) readNumber() int {
       return generators[int(g)].Intn(10)
    }
    

    这里是追踪信息:

    而且,goroutine 是在函数调用开始阶段被抢占的:

    这个检查过程是由编译器自动加入的;这里有一段上例生成的汇编代码:

    通过将指令插入在每个函数执行前,这个 runtime 调用确保栈可以增加。同时使得调度器在必要时可以运行。

    绝大多数情况下,goroutine 都会给调度器对它们进行调度的能力。但是,一个没有函数调用的循环却可以阻塞调度。

    查看全文

    4 条回复    2020-03-11 13:35:42 +08:00
    hjc4869
        1
    hjc4869  
       2020-03-11 05:00:14 +08:00 via Android
    本质上还是主动 yield,编译器帮你插指令而已
    chevalier
        2
    chevalier  
       2020-03-11 10:13:39 +08:00
    请教一下,那个时序图是怎么搞出来的?
    darluc
        3
    darluc  
    OP
       2020-03-11 11:09:57 +08:00
    reus
        4
    reus  
       2020-03-11 13:35:42 +08:00
    @hjc4869 文章说的已经过时了,1.14 可以用信号来抢占
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5905 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 02:09 · PVG 10:09 · LAX 19:09 · JFK 22:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.