...
实现协程调度之后,可以解决前一章协程模块中子协程不能运行另一个子协程的缺陷,子协程可以通过向调度器添加调度任务的方式来运行另一个子协程。
协程调度最难理解的地方是user caller线程时调度协程和主线程切换的情况,注意对照源码进行理解。协程调度最难理解的地方是当caller线程也参与调度时调度协程和主线程切换的情况,注意对照源码进行理解。
协程调度概述
当你有很多协程时,如何把这些协程都消耗掉,这就是协程调度。
...
接下来是调度器如何运行,这里可以简单地认为,调度器创建后,内部首先会创建一个调度线程池,调度开始后,所有调度线程按顺序从任务队列里取任务执行,调度线程数越多,能够同时调度的任务也就越多,当所有任务都调度完后,调度线程就停下来等新的任务进来。
接下来是添加调度任务,添加调度任务的本质就是往调度器的任务队列里塞任务,但是,只添加调度任务是不够的,还应该有一种方式用于通知调度线程有新的任务加进来了,因为调度线程并不一定知道有新任务进来了。接下来是添加调度任务,添加调度任务的本质就是往调度器的任务队列里塞任务,但是,只添加调度任务是不够的,还应该有一种方式用于通知调度线程有新的任务加进来了,因为调度线程并不一定知道有新任务进来了。调度线程当然也可以不停地轮询有没有新任务,但是这样CPU占用率会很高。
接下来是调度器的停止。调度器应该支持停止调度的功能,以便回收调度线程的资源。接下来是调度器的停止。调度器应该支持停止调度的功能,以便回收调度线程的资源,只有当所有的调度线程都结束后,调度器才算真正停止。
通过上面的描述,一个协程调度器的大概设计也就出炉了:
...
首先是协程调度器的初始化。sylar的协程调度器在初始化时支持传入线程数和一个布尔型的use_caller参数,表示是否使用caller线程。在使用caller线程的情况下,线程数自动减一,并且调度器内部会初始化caller线程的调度协程并保存起来(比如,在main函数中创建的调度器,如果usecaller参数,表示是否使用caller线程。在使用caller线程的情况下,线程数自动减一,并且调度器内部会初始化一个属于caller线程的调度协程并保存起来(比如,在main函数中创建的调度器,如果use_caller为true,那调度器会记录main函数所在线程的调度协程)。caller为true,那调度器会初始化一个属于main函数线程的调度协程)。
调度器创建好后,即可调用调度器的schedule方法向调度器添加调度任务,但此时调度器并不会立刻执行这些任务,而是将它们保存到内部的一个任务队列中。
...
接下来是调度器的停止。调度器的停止行为要分两种情况讨论,首先是use_caller为false的情况,这种情况下,没有使用caller线程进行调度,那么只需要简单地等各个调度线程的调度协程退出就行了。如果usecaller为false的情况,这种情况下,由于没有使用caller线程进行调度,那么只需要简单地等各个调度线程的调度协程退出就行了。如果use_caller为true,表示caller线程也要参考调度,这时,调度器初始化时记录的caller线程的调度协程就要起作用了,在调度器停止前,应该让这个caller线程的调度协程也运行一次,让caller线程完成调度工作后再退出。如果调度器只使用了caller线程进行调度,那么所有的调度任务要在调度器停止时才会被调度。caller为true,表示caller线程也要参考调度,这时,调度器初始化时记录的属于caller线程的调度协程就要起作用了,在调度器停止前,应该让这个caller线程的调度协程也运行一次,让caller线程完成调度工作后再退出。如果调度器只使用了caller线程进行调度,那么所有的调度任务要在调度器停止时才会被调度。
调度协程切换问题
这里分两种情况讨论一下调度协程的切换情况,其他情况可以看成以下两种情况的组合,原理是一样的。
1. 线程数为1,且use_caller为true,对应只使用main函数的线程进行协程调度。caller为true,对应只使用main函数线程进行协程调度的情况。
2. 线程数为1,且use_caller为false,对应只创建一个线程进行协程调度,main函数线程不参与调度。caller为false,对应额外创建一个线程进行协程调度、main函数线程不参与调度的情况。
情况2比较好理解,因为有单独的线程用于协程调度,那只需要让新线程运行调度协程就可以了,main函数与协程调度完全不相关,main函数只需要向调度器添加任务,然后在适当的时机停止调度器,当调度器停止时,main函数要等待调度线程结束后再退出,参考下面的图示:情况2比较好理解,因为有单独的线程用于协程调度,那只需要让新线程的入口函数作为调度协程,从任务队列里取任务执行就行了,main函数与调度协程完全不相关,main函数只需要向调度器添加任务,然后在适当的时机停止调度器即可。当调度器停止时,main函数要等待调度线程结束后再退出,参考下面的图示:
Graphviz | ||
---|---|---|
| ||
digraph {
rankdir=LR;
node [style=filled];
compound=true;
subgraph sub1 {
start [label="main函数开始" shape=none style=""];
main1 [label = "创建调度器"];
main2 [label = "开始调度"];
main3 [label = "添加调度任务"];
main4 [label = <停止调度<BR/>(等待调度线程退出)>];
end [label="main函数结束" shape=none style=""];
rank=same;
start -> main1 -> main2 -> main3 -> main4 -> end;
}
subgraph sbu2 {
scheduler1 [label="调度协程" style=filled];
scheduler2 [label="调度协程" style=filled];
scheduler3 [label="调度协程" style=filled];
scheduler4 [label="调度协程" style=filled]
scheduler_end [label="结束"]
child1 [label="子协程1"]
child2 [label="子协程2"]
child3 [label="子协程3"]
{rank=same; scheduler1 scheduler2 scheduler3 scheduler4 scheduler_end}
{rank=same; child1 child2 child3}
}
scheduler1->child1->scheduler2->child2->scheduler3->child3->scheduler4->scheduler_end;
main2->scheduler1[label="创建调度线程"];
} |
情况1则比较复杂,因为没有额外的线程进行协程调度,那只能用main函数所在的线程来进行调度,而梳理一下这个线程要运行的协程,会发现有以下三类协程:
1. main函数对应的主协程
2. 调度协程
3. 待调度的任务协程
在main函数线程里这三类协程运行的顺序是这样的:
1. main函数主协程运行,创建调度器
2. 仍然是main函数主协程运行,向调度器添加一些调度任务
3. 开始协程调度,main函数主协程让出执行权,切换到调度协程,调度协程从任务队列里按顺序执行所有的任务
4. 每次执行一个任务,调度协程都要让出执行权,再切到该任务的协程里去执行,任务执行结束后,还要再切回调度协程,继续下一个任务的调度
5. 所有任务都执行完后,调度协程还要让出执行完并切回main函数主协程,以保证程序能顺利结束。
上面的过程也可以总结为:main函数先攒下一波协程,然后切到调度协程里去执行,等把这些协程都消耗完后,再从调度协程切回来,像下面这样:
Graphviz | ||
---|---|---|
| ||
digraph { rankdir=LR; node [style=filled]; compound=true; subgraph sub1 { start [label="main函数开始" shape=none style=""]; main1 [label = "创建调度器"]; main2 [label = <开始调度<BR/>(实际什么也没做)>]; main3 [label = "添加调度任务"]; main4 [label = <停止调度<BR/>>]; end [label="main函数结束" shape=none style=""]; rank=same; start -> main1 -> main2 -> main3 -> main4 -> end; } subgraph sbu2 { scheduler1 [label="调度协程" style=filled]; scheduler2 [label="调度协程" style=filled]; scheduler3 [label="调度协程" style=filled]; scheduler4 [label="调度协程" style=filled] child1 [label="子协程1"] child2 [label="子协程2"] child3 [label="子协程3"] {rank=same; scheduler1 scheduler2 scheduler3 scheduler4} {rank=same; child1 child2 child3} } scheduler1->child1->scheduler2->child2->scheduler3->child3->scheduler4; main4 -> scheduler1 [label="切到调度协程"]; scheduler4 -> main4 [label=<全部任务执行结束后<BR/>调度协程返回main函数主协程>]; } |
sylar协程模块运行图示
然后描述一下协程调度的本质。
...