版本比较

标识

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。

...

实现协程调度之后,可以解决前一章协程模块中子协程不能运行另一个子协程的缺陷,子协程可以通过向调度器添加调度任务的方式来运行另一个子协程。

协程调度最难理解的地方是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
UUIDgraphviz_container_use_caller_false
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
UUIDgraphviz_container_use_caller_true
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协程模块运行图示


然后描述一下协程调度的本质。

...