cmatrixprobe

发呆业务爱好者

Goroutine调度(本地执行队列顺序)

Go in action应该算是学习Go语言必读的一本好书,全书通俗易懂,非常适合看完基础语法后进一步学习。不同于Java和C++的1:1线程模型,Go使用调度器实现了m:n的MPG模型,即m个goroutine对应n个os线程。在Go in action第六章并发中简要叙述了MPG调度模型,其中M:操作系统内核级线程,P:逻辑处理器,G:Goroutine(以下均用简写代替)。每一个P会绑定到一个M,当创建一个G后,G会先被放到调度器的全局运行队列中,之后调度器会将全局运行队列中的G分配给不同的P,并放到P的本地运行队列中等待执行。
Go调度器如何管理goroutine
如上图,当正在运行的G4需要执行一个阻塞的系统调用,P0会与M2和G4分离,调度器会创建一个新的线程M3与分离的P0绑定,继续执行本地队列中的G5;而G4会一直阻塞直到系统调用返回。


当使用多于一个P时,调度器将全局运行队列中的G平等分配到每个P,让Goroutine在不同线程上运行。但若想真正实现并行,需要将程序运行在多核物理处理器上,这时每个M运行在不同物理层面的处理器,从而实现并行。

误区

有些书或者博客中认为runtime.GOMAXPROCS设置的是最大处理器核数,其实是不严谨的。它设置的是逻辑处理器数量,同时每个逻辑处理器又绑定到单个os线程,也就是m:n中的n。只不过为了避免上下文切换的开销,其默认值设为处理器核数。

执行顺序

Goroutine采用的是半抢占式的协作调度,只有在当前Goroutine阻塞时才会导致调度

2020/5/1
1.14版本已经实现了基于信号的抢占调度

可能很多人会像我一样有疑惑,对于每个P,调度器是如何决定队列中G的执行顺序的?

实践是检验真理的唯一标准,这里我写了一个main函数,首先指定GOMAXPROCS为1,控制P只有一个,然后写一个用于打印函数分别打印A和B。

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var wg sync.WaitGroup

func main() {
    runtime.GOMAXPROCS(1)
    wg.Add(2)
    go print("A")
    go print("B")
    wg.Wait()
}

func print(c string) {
    defer wg.Done()
    for i := 0; i < 100; i++ {
        fmt.Print(c)
    }
}

输出结果:

可以看到程序先输出了B,难道是从后向前执行吗?


试一试多输出两个,main函数改成:

func main() {
    runtime.GOMAXPROCS(1)
    wg.Add(4)
    go print("A")
    go print("B")
    go print("C")
    go print("D")
    wg.Wait()
}

输出结果:

这里大概可以找出规律,首先执行主goroutine中最后一个,然后再从头开始顺序执行,但是如果主goroutine本身需要执行print呢?


在中间进行主goroutine的输出,这里要注意的是wg数量仍然为4,否则会死锁。

func main() {
    runtime.GOMAXPROCS(1)
    wg.Add(4)
    go print("A")
    go print("B")
    for i := 0; i < 100; i++ {
        fmt.Print("E")
    }
    go print("C")
    go print("D")
    wg.Wait()
}

输出结果:

主goroutine中的E首先被打印。

结论

可以看到当设置逻辑处理器数量为1时,执行优先级为主goroutine > 子goroutine中最后一个 > 其他子goroutine按顺序排列,但也可能有极小概率出现顺序不一致的情况。其中无论子goroutine调用的是不是匿名函数结果都是一样的。

Go+Vue | vscode扩展和配置

上一篇

剑指Offer43题 | 1~n整数中1出现的次数

下一篇
评论
头像 发表评论 说点什么
还没有评论
1173
0