Go routine调度

66次阅读

共计 2501 个字符,预计需要花费 7 分钟才能阅读完成。

go routine 的调度原理和操作系统的线层调度是比较相似的。这里我们将介绍 go routine 的相关知识。
goroutine(有人也称之为协程)本质上 go 的用户级线程的实现,这种用户级线程是运行在内核级线程之上。当我们在 go 程序中创建 goroutine 的时候,我们的这些 routine 将会被分配到不同的内核级线程中运行。一个内核级线程可能会负责多个 routine 的运行。而保证这些 routine 在内内核级线程安全、公平、高效运行的工作,就由调度器来实现。
Go 调度的组成
Go 的调度主要有四个结构组成,分别是:

G:goroutine 的核心结构,包括 routine 的栈、程序计数器 pc、以及一些状态信息等;
M:内核级线程。goroutine 在 M 上运行。M 中信息包括:正在运行的 goroutine、等待运行的 routine 列表等。当然也包括操作系统线程相关信息,这些此处不讨论。
P:processor,处理器,只要用于执行 goroutine,维护了一个 goroutine 列表。其实 P 是可以从属于 M 的。当 P 从属于(分配给)M 的时候,表示 P 中的某个 goroutine 得以运行。当 P 不从属于 M 的时候,表示 P 中的所有 goroutine 都需要等待被安排到内核级线程运行。
Sched:调度器,存储、维护 M,以及一个全局的 goroutine 等待队列,以及其他状态信息。

Go 程序的启动过程

初始化 Sched:一个存储 P 的列表 pidle。P 的数量可以通过 GOMAXPROCS 设置;
创建第一个 goroutine。这个 goroutine 会创建一个 M,这个内核级线程(sysmon)的工作是对 goroutine 进行监控。之后,这个 goroutine 开始我们在 main 函数里面的代码,此时,该 goroutine 就是我们说的主 routine。

创建 goroutine:

goroutine 创建时指定了代码段
然后,goroutine 被加入到 P 中去等待运行。
这个新建的 goroutine 的信息包含:栈地址、程序计数器

创建内核级线程 M
内核级线程由 go 的运行时根据实际情况创建,我们无法再 go 中创建内核级线程。那什么时候回创建内核级线程呢?当前程序等待运行的 goroutine 数量达到一定数量及存在空闲(为被分配给 M)的 P 的时候,Go 运行时就会创建一些 M,然后将空闲的 P 分配给新建的内核级线程 M,接着才是获取、运行 goroutine。创建 M 的接口函数如下:
// 创建 M 的接口函数
void newm(void (*fn)(void), P *p)

// 分配 P 给 M
if(m != &runtime·m0) {Â
acquirep(m->nextp);
m->nextp = nil;
}
// 获取 goroutine 并开始运行
schedule();
M 的运行
static void schedule(void)
{
G *gp;

gp = runqget(m->p);
if(gp == nil)
gp = findrunnable();

// 如果 P 的类别不止一个 goroutine,且调度器中有空闲的的 P,就唤醒其他内核级线程 M
if (m->p->runqhead != m->p->runqtail &&
runtime·atomicload(&runtime·sched.nmspinning) == 0 &&
runtime·atomicload(&runtime·sched.npidle) > 0) // TODO: fast atomic
wakep();
// 执行 goroutine
execute(gp);
}

runqget: 从 P 中获取 goroutine 即 gp。gp 可能为 nil(如 M 刚创建时 P 为空;或者 P 的 goroutine 已经运行完了)。
findrunnable:寻找空闲的 goroutine(从全局的 goroutine 等待队列获取 goroutine;如果所有 goroutine 都已经被分配了,那么从其他 M 的 P 的 goroutine 的 goroutine 列表获取一些)。如果获取到 goroutine,就将他放入 P 中,并执行它;否则没能获取到任何的 goroutine,该内核级线程进行系统调用 sleep 了。
wakep:当当前内核级线程 M 的 P 中不止一个 goroutine 且调度器中有空闲的的 P,就唤醒其他内核级线程 M。(为了找些空闲的 M 帮自己分担)。

Routine 状态迁移
前面说的是 G,M 是怎样创建的以及什么时候创建、运行。那么 goroutine 在 M 是是怎样进行调度的呢?这个才是 goroutine 的调度核心问题,即上面代码中的 schedule。在说调度之前,我们必须知道 goroutine 的状态有什么,以及各个状态之间的关系。

Gidle:创建中的 goroutine,实际上这个状态没有什么用;
Grunnable:新创建完成的 goroutine 在完成了资源的分配及初始化后,会进入这个状态。这个新创建的 goroutine 会被分配到创建它的 M 的 P 中;
Grunning:当 Grunnable 中的 goroutine 等到了空闲的 cpu 或者到了自己的时间片的时候,就会进入 Grunning 状态。这个装下的 goroutine 可以被前文提到的 findrunnable 函数获取;
Gwaiting:当正在运行的 goroutine 进行一些阻塞调用的时候,就会从 Grunning 状态进入 Gwaiting 状态。常见的调用有:写入一个满的 channel、读取空的 channel、IO 操作、定时器 Ticker 等。当阻塞调用完成后,goroutine 的状态就会从 Gwaiting 转变为 Grunnable;
Gsyscall:当正在运行的 goroutine 进行系统调用的时候,其状态就会转变为 Gsyscall。当系统调用完成后 goroutine 的状态就会变为 Grunnable。(前文提到的 sysmon 进程会监控所有的 P,如果发现有的 P 的系统调用是阻塞式的或者执行的时间过长,就会将 P 从原来的 M 分离出来,并新建一个 M,将 P 分配给这个新建的 M)。

Ref

https://zhuanlan.zhihu.com/p/…
http://skoo.me/go/2013/11/29/…

正文完
 0