ew
cyber提供的三大功能:调度功能、通讯功能、组件功能。其中调度功能最核心的逻辑在/cyber/scheduler下面,本篇文章主要自己对该模块的研读和理解,可能会有错误,看到的朋友帮忙点播一下。
首先看下牵涉到协程的一些基本概念图谱,对协程有些基本的概念和了解
再次看下阿波罗设计的协程-调度系统类关系图谱,这个理解这个图谱会对阅读源代码有所帮助,推荐边看源代码边看图谱
里面有两大调度方案:classic(经典)和choreography(编排),图谱如下
进入正文
cyber主要提供了两种协程的调度方案classic(经典)和choreography(编排),通过配置文件对相关参数的初始化。首先看下该模块的组成成员
1.common文件夹
2.policy文件夹
3.ProcessorContext类(processor_context.h和processor_context.cc文件,为了方便以后用类来代表该两个文件内容)
4.Processor类
5.scheduler_factory类
6.Scheduler类
7.所有单元测试文件(所有测试文件不做细节分析了)
下面一一讲解上面所述的组成单元
1.common文件夹
该文件下主要提供的是一些辅助工具类,包括CvWrapper、MutexWrapper、pin_thread
CvWrapper和MutexWrapper主要是对std::condition_variable和std::mutex最简单的封装,禁用赋值构造函数,提供获取std对象的接口,包含一个std对象的成员。对std对象这么封装,说实话我是没太理解作者这么写的意图,仅仅就为了禁用赋值构造函数?要是仅仅这个原因大可不必吧,使用的时候注意点不对std对象赋值构造就可以了吧?pin_thread主要是提供了三个公共函数,具体原理在之前的文章已写过,就是调用系统api设置线程和CPU的亲和性,设置线程的调度策略和优先级
2.policy文件夹
该文件下主要提供一些多态的具体实现,包括SchedulerClassic、SchedulerChoreography、ClassicContext、ChoreographyContext。
3.ProcessorContext类
是一个抽象类,有两个多态的实现ClassicContext、ChoreographyContext,设计目的:作为Processor的私有属性的成员,提供三个虚函数接口:Shutdown、NextRoutine、Wait
Shutdown:停止执行器
NextRoutine:获取下一个协程
Wait:没活干了,阻塞执行器,等待活干
4.Processor类
执行器类,主要成员std::thread和ProcessorContext,std::thread用来创建线程去干活,ProcessorContext来切换该干哪个活了。ProcSnapshot函数提供执行器的快照,快照内容主要是协程信息
5.Scheduler类
调度器基类,有以下成员
4个虚函数:RemoveTask、DispatchTask、NotifyProcessor、RemoveCRoutine,这四个虚函数都没有默认实现,意味着此类不可以直接实例化,只能实例化其子类。RemoveTask和RemoveCRoutine都是将协程从调度队列里面移除的意思,不同的两者参数一个是名字一个是协程id,id是协程名字哈希值。移除协程的时候会将协程的force_stop_置为true,意为下次不参与调度,如果当前协程正在运行估计是停止不了。DispatchTask简而言之就是将协程加入到调度队列里面。NotifyProcessor逻辑简而言之就是调度器先切到一个协程然后调用其Notify告诉他可以接着执行了
构造函数和析构函数:空实现
静态Instance接口:空实现,实际实例化的时候使用一个在apollo::cyber::scheduler命令空间下的Instance()接口去实例化的
CreateTask函数:传入协程执行体和协程名字和DataVisitorBase对象,主要逻辑创建协程,并将其加入到任务队列(调用DispatchTask)
ProcessLevelResourceControl函数:调度器自己的线程cpu亲和性
SetInnerThreadAttr和SetInnerThreadConfs:这两个是设置特殊线程的优先级和cpu亲和性和调度策略的,以线程名字为准,比如日志线程可以利用这套函数进行设置
CheckSchedStatus:输出所有执行器当前的状态,唯一调用者/cyber/sysmo模块
Shutdown:关闭调度器,释放所有协程,停止所有执行器,清空其他资源
TaskPoolSize:获取任务池大小,在经典调度模式中该值等于执行器个数,在编排调度模式中该值一般会配置文件读取
id_cr_和id_cr_lock_:协程id的map和其锁
id_map_mutex_和cr_wl_mtx_:每个协程的锁组成的map和这个map的锁
pctxs_:context数组
processors_:执行器数组
inner_thr_confs_:非协程线程的属性配置
process_level_cpuset_:调度线程CPU亲和性
proc_num_:执行器个数
task_pool_size_:任务池个数,/cyber/task模块下有个单例TaskManager,构造的时候会创建task_pool_size_个数协程,这些协程执行体就是:从任务队列里面拿出来任务去执行
stop_:是否停止标志位
6.SchedulerChoreography类
成员变量:优先级、CPU亲和性、调度策略、CPU集合都分别有两套,一套是默认的,一套是pool系列的,另外有个cr_confs_,这个是预先定义指定task名称具备的属性(优先级和绑定到哪个执行器上面)
构造函数:根据配置文件对所有成员变量进行初始化。调用CreateProcessor创建执行器
CreateProcessor函数:根据成员属性创建两套执行器(一套默认的ChoreographyContext,一套pool系列的ClassicContext),
DispatchTask函数:创建协程对应的互斥锁,根据协程名字定义协程属性,将协程加入到协程map方便协程启动和停止和移除,将协程添加到调度队列里面从而进行调度
7.ChoreographyContext类(一个执行器对应一个ChoreographyContext)
虽然系统默认是classic的调度模式,classic模式也是简单的模式,但是代码上理解起来还是比Choreography复杂了一点,从简单的看起吧。来看下ChoreographyContext类有那些成员,有三个虚函数的实现NextRoutine、Wait、Shutdown,有三个个公开函数RemoveCRoutine、Enqueue、Notify,5个私有变量mtx_wq_、cv_wq_、notify、rq_lk_、cr_queue_,5个变量中前3个为一组,后两个为一组。下面我们一一介绍这些成员作用和目的。
前三个成员变量和成员函数Notify、Wait、Shutdown放在一起讨论:因为他们是一套逻辑,即控制调度器的资源管理,大白话讲就是,三个变量就是控制所有协程运行完毕了控制执行器的阻塞,来活了赶紧干活,notify变量其实就是个标志位,代表是否有活干,锁和条件变量就是为了控制阻塞,Notify函数逻辑就是notify加一并通知条件变量,Wait函数逻辑就是死等notify,检测到了notify等于1,将notify置为0并进行返回,Shutdown逻辑为将notify置为128并通知条件变量。
cr_queue_:一个multimap类型,key储存的是协程优先级,value储存的是协程,map按照优先级从低到高排序,这里优先级高的先执行就得到了保障,multi意为同等权限的协程会有多个
rq_lk_:控制cr_queue_的锁
RemoveCRoutine:ChoreographyContext类包含多个协程成员,要想移除其中一个就调用此函数,唯一的调用者SchedulerChoreography类的RemoveCRoutine函数。移除逻辑:在map里面找到该协程,协程停止,获得协程锁(保证其他人没有在使用中),裁判从map中移除,释放协程锁资源
Enqueue:将协程放进map里面
NextRoutine:获取下一个协程,意为上个协程已执行完毕或者进行了yield操作,执行下一个,这里值的注意的是,返回协程的时候该协程处于被锁状态,即其他方面对该协程进行Acquire会返回false
8.SchedulerClassic类
cr_confs_:从配置文件任务名->任务属性读取到信息
classic_conf_:经典调度器所有配置
构造函数:从配置文件初始化相关变量,创建执行器
CreateProcessor函数:创建执行器,值的注意的是创建执行器的个数为:分组个数*每个分组执行器个数的和,设置执行器和cpu的亲和性和调度策略
DispatchTask函数:分派任务,创建协程对应的互斥锁,将协程加入到协程map方便协程启动和停止和移除,根据协程的名字设置其优先级和分组名字,若该协程没有在配置文件里面备案过,则将其加入到第一个分组里面。根据协程的分组名字和执行权限,将其加入到对应的ClassicContext::cr_group_里面
NotifyProcessor函数:将协程设置为READY状态,调用该分组的ClassicContext::Notify
9.ClassicContext类(一个执行器对应一个ChoreographyContext)
有一说一相比ChoreographyContext来说ClassicContext更难理解,虽然经典模式是比较基础的调度模式,编排模式是比较高级点的模式。有两个成员属性wake_time和need_sleep_用不到就不再提了。有5个共有静态成员属性,为所有ClassicContext共用属性;5个私有变量属性,做下详细分析
------------------共有静态成员-------------------------
为什么采用静态属性?
答:一个context绑定一个执行器,即一个线程,静态属性意思为所有线程共用一套协程队列,即任意一个协程可能在改组定义的任何一个线程执行
cr_group_:协程组,按组名划分的一大堆协程,每个组又按照优先(20个)划分的小组,每个小组有若干个协程
rq_locks_:读写锁组,按组名划分的一大堆读写锁,每个组按照优先级(20个)划分的小组,每个小组有1个读写锁
cv_wq_:条件变量组,按组名划分的一大堆条件变量,每个组有一个条件变量
mtx_wq_:互斥锁组,按组名划分的一大堆互斥锁,每个组有一个互斥锁
notify_grp_:时间通知标识,按组名划分的一大堆标识,每个组有一个标识
------------------私有非静态成员-----------------------
multi_pri_rq_:指针,指向当前分组名称的协程组,
lq_:指针,指向当前分组名称的读写锁组,
mtx_wrapper_:指针,指向当前分组名称的互斥锁
cw_:指针,指向当前分组名称的条件变量
current_grp:当前分组名称
静态成员均为共有成员,会在SchedulerClassic和SchedulerChoreography的DispatchTask函数里面添加数据,非静态成员主要是指针指向静态成员里面当前实例的相关数据。成员函数和SchedulerChoreography类相似
InitGroup函数:初始化静态成员里面当前实例的位置,初始化非静态指针的指向
NextRoutine函数:从当前协程分组里面挨个取出协程(疑问:为什么按照权限值从大到小的顺序去取)
Wait函数:阻塞执行器
Shutdown函数:停止执行器
Notify静态函数:解除阻塞执行器
RemoveCRoutine静态函数:删除指定协程
其协程储存结构如下:
============================华丽分割线=====================================
前面我们是对cyber的调度系统咔咔的一顿分析,有一说一,比较抽象,难以理解,下面我们做一些测试例子,来进一步感受以下调度系统在做什么。
1.首先讲述以下接下来测试要用到的命令
查看进程信息
top -d 0.3 (0.3秒刷新一次)
查看线程信息
top -H -p 30852(进程id)
查看线程亲和性
cat /proc/27787(进程id)/task/27821(线程id)/status
选中字段即是亲和性的结果
查看线程的调度策略和优先级等状态信息
cat /proc/30852(进程id)/task/30886(线程id)/sched
选中字段即是调度属性和优先级属性
2.测试工程的建立和测试方法
在/apollo/cyber目录下新建tt目录,里面包括BUILD文件和main.cc主文件。
在/apollo/conf目录下新建文件tt.conf作为启动配置文件
运行测试工程命令可以用命令
bazel run //cyber/tt:cyber_test
也可以用命令
sudo /apollo/.cache/bazel/540135163923dd7d5820f3ee4b306b32/execroot/apollo/bazel-out/k8-fastbuild/bin/cyber/tt/cyber_test
推荐使用后者,因为调用某些系统API需要sudo权限,使用bazel那一套很可能不生效
3.测试工程
3.1测试:测试两个调度器的公共部分,即设置主线程和内部线程(非协程)的属性。主要用到以下两个字段的属性
process_level_cpuset字段:主线程的CPU亲和性
threads字段:内部线程的属性
main.cc代码如下:
#include <sys/syscall.h>
#include <chrono>
#include <iostream>
#include <thread>
#include "cyber/common/global_data.h"
#include "cyber/cyber.h"
#include "cyber/scheduler/common/pin_thread.h"
#include "cyber/scheduler/policy/choreography_context.h"
#include "cyber/scheduler/processor.h"
using namespace std;
using namespace std::chrono_literals;
using namespace apollo::cyber::scheduler;
using apollo::cyber::proto::InnerThread;
using apollo::cyber::scheduler::ChoreographyContext;
using apollo::cyber::scheduler::Processor;
//参数传1的时候cpu运算需要1-2秒
int64_t cpu_run(int c) {
int64_t sum = 0;
for (int count = 0; count < c; count++) {
for (int i = 0; i < 1024; i++) {
for (int j = 0; j < 1024; j++) {
for (int k = 0; k < 1024; k++) {
sum += i * j * k;
}
}
}
}
return sum;
}
int main(int argc, char* argv[]) {
apollo::cyber::common::GlobalData::Instance()->SetProcessGroup("tt");
apollo::cyber::Init(argv[0]);
std::thread* pThread = new std::thread([]() {
while (apollo::cyber::OK()) {
auto time = apollo::cyber::Time::Now();
std::cout << " --- " << time << std::endl;
// std::this_thread::sleep_for(1s);
cpu_run(1);
}
});
apollo::cyber::scheduler::Instance()->SetInnerThreadAttr("test_thread",
pThread);
cpu_run(1);
apollo::cyber::WaitForShutdown();
return 0;
}
tt.conf代码如下
scheduler_conf {
policy: "choreography"
process_level_cpuset: "1" # all threads in the process are on the cpuset
#如果processor_policy设置为SCHED_FIFO/SCHED_RR,processor_prio取值为(1-99),值越大,表明优先级越高,抢到cpu概率越大。
#如果processor_policy设置为SCHED_OTHER,processor_prio取值为(-20-19,0为默认值),这里为nice值,nice值不影响分配到cpu的优先级,但是影响分到cpu时间片的大小,如果nice值越小,分到的时间片越多。
#上面两句话抄写子阿波罗官方文档,是对linux的API pthread_setschedparam 的解释
threads: [
{
name: "test_thread"
cpuset: "4"
policy: "SCHED_OTHER"
prio: 10
}
]
}
测试结果:cpu亲和性可以清晰的看到结果,调度策略和优先级可以用命令查看到结果,但是尚不能直接感受到区别。
3.2 经典模式和编排模式测试代码和结果
经典模式测试源码和配置文件
#include <sys/syscall.h>
#include <chrono>
#include <iostream>
#include <thread>
#include "cyber/common/global_data.h"
#include "cyber/cyber.h"
#include "cyber/scheduler/common/pin_thread.h"
#include "cyber/scheduler/policy/choreography_context.h"
#include "cyber/scheduler/processor.h"
using namespace std;
using namespace std::chrono_literals;
using namespace apollo::cyber::scheduler;
using apollo::cyber::proto::InnerThread;
using apollo::cyber::scheduler::ChoreographyContext;
using apollo::cyber::scheduler::Processor;
//参数传1的时候cpu运算需要1-2秒
int64_t cpu_run(int c) {
int64_t sum = 0;
for (int count = 0; count < c; count++) {
for (int i = 0; i < 1024; i++) {
for (int j = 0; j < 1024; j++) {
for (int k = 0; k < 1024; k++) {
sum += i * j * k;
}
}
}
}
return sum;
}
int id = 1;
void fun(std::string name) {
int runtimes = 0;
while (apollo::cyber::OK() && runtimes < 10) {
static std::set<std::thread::id> thread_set;
thread_set.insert(std::this_thread::get_id());
std::cout << name << " "
<< id++ << " "
<< "this_thread::get_id():" << std::this_thread::get_id()
<< " syscall(SYS_gettid):" << syscall(SYS_gettid)
<< " pthread_self():" << pthread_self()
<< " thread count:" << thread_set.size();
static std::set<apollo::cyber::croutine::CRoutine*> croutine_set;
apollo::cyber::croutine::CRoutine* r =
apollo::cyber::croutine::CRoutine::GetCurrentRoutine();
if (r != nullptr) {
croutine_set.insert(r);
std::cout << " croutine_id:" << r->id()
<< " group_name:" << r->group_name() << " GetCurrentRoutine:"
<< apollo::cyber::croutine::CRoutine::GetCurrentRoutine()
<< " croutine name:" << r->name()
<< " processor_id:" << r->processor_id()
<< " croutine count: " << croutine_set.size() << std::endl;
} else {
std::cout << std::endl;
}
apollo::cyber::USleep(1* 100 * 1000);
cpu_run(1);
runtimes ++;
}
}
int main(int argc, char* argv[]) {
apollo::cyber::common::GlobalData::Instance()->SetProcessGroup("tt");
apollo::cyber::Init(argv[0]);
apollo::cyber::Async(fun, "A");
apollo::cyber::Async(fun, "B");
apollo::cyber::Async(fun, "C");
apollo::cyber::scheduler::Instance()->CreateTask(std::bind(fun, "A"), "A");
apollo::cyber::scheduler::Instance()->CreateTask(std::bind(fun, "B"), "B");
apollo::cyber::scheduler::Instance()->CreateTask(std::bind(fun, "C"), "C");
apollo::cyber::WaitForShutdown();
return 0;
}
scheduler_conf {
policy: "classic"
process_level_cpuset: "0-7,16-23" # all threads in the process are on the cpuset
threads: [
{
name: "async_log"
cpuset: "1"
policy: "SCHED_OTHER" # policy: SCHED_OTHER,SCHED_RR,SCHED_FIFO
prio: 0
}, {
name: "shm"
cpuset: "2"
policy: "SCHED_FIFO"
prio: 10
}
]
classic_conf {
groups: [
{
name: "group1"
processor_num: 16
affinity: "range"
cpuset: "0-7"
processor_policy: "SCHED_OTHER" # policy: SCHED_OTHER,SCHED_RR,SCHED_FIFO
processor_prio: 0
tasks: [
{
name: "E"
prio: 0
}
]
},{
name: "group2"
processor_num: 10
affinity: "1to1"
cpuset: "1-10"
processor_policy: "SCHED_OTHER"
processor_prio: 0
tasks: [
{
name: "A"
prio: 1
},{
name: "B"
prio: 1
},{
name: "C"
prio: 2
},{
name: "D"
prio: 3
}
]
}
]
}
}
编排模式测试源码和配置文件
#include <sys/syscall.h>
#include <chrono>
#include <iostream>
#include <thread>
#include "cyber/common/global_data.h"
#include "cyber/cyber.h"
#include "cyber/scheduler/common/pin_thread.h"
#include "cyber/scheduler/policy/choreography_context.h"
#include "cyber/scheduler/processor.h"
using namespace std;
using namespace std::chrono_literals;
using namespace apollo::cyber::scheduler;
using apollo::cyber::proto::InnerThread;
using apollo::cyber::scheduler::ChoreographyContext;
using apollo::cyber::scheduler::Processor;
//参数传1的时候cpu运算需要1-2秒
int64_t cpu_run(int c) {
int64_t sum = 0;
for (int count = 0; count < c; count++) {
for (int i = 0; i < 1024; i++) {
for (int j = 0; j < 1024; j++) {
for (int k = 0; k < 1024; k++) {
sum += i * j * k;
}
}
}
}
return sum;
}
int id = 1;
void fun(std::string name) {
int runtimes = 0;
while (apollo::cyber::OK() && runtimes < 10) {
static std::set<std::thread::id> thread_set;
thread_set.insert(std::this_thread::get_id());
std::cout << name << " "
<< id++ << " "
<< "this_thread::get_id():" << std::this_thread::get_id()
<< " syscall(SYS_gettid):" << syscall(SYS_gettid)
<< " pthread_self():" << pthread_self()
<< " thread count:" << thread_set.size();
static std::set<apollo::cyber::croutine::CRoutine*> croutine_set;
apollo::cyber::croutine::CRoutine* r =
apollo::cyber::croutine::CRoutine::GetCurrentRoutine();
if (r != nullptr) {
croutine_set.insert(r);
std::cout << " croutine_id:" << r->id()
<< " group_name:" << r->group_name() << " GetCurrentRoutine:"
<< apollo::cyber::croutine::CRoutine::GetCurrentRoutine()
<< " croutine name:" << r->name()
<< " processor_id:" << r->processor_id()
<< " croutine count: " << croutine_set.size() << std::endl;
} else {
std::cout << std::endl;
}
apollo::cyber::USleep(1* 100 * 1000);
cpu_run(1);
runtimes ++;
}
}
int main(int argc, char* argv[]) {
apollo::cyber::common::GlobalData::Instance()->SetProcessGroup("tt");
apollo::cyber::Init(argv[0]);
apollo::cyber::Async(fun, "A");
apollo::cyber::Async(fun, "B");
apollo::cyber::Async(fun, "C");
apollo::cyber::scheduler::Instance()->CreateTask(std::bind(fun, "A"), "A");
apollo::cyber::scheduler::Instance()->CreateTask(std::bind(fun, "B"), "B");
apollo::cyber::scheduler::Instance()->CreateTask(std::bind(fun, "C"), "C");
apollo::cyber::WaitForShutdown();
return 0;
}
scheduler_conf {
policy: "choreography"
process_level_cpuset: "0-7,16-23" # all threads in the process are on the cpuset
threads: [
{
name: "lidar"
cpuset: "1"
policy: "SCHED_RR" # policy: SCHED_OTHER,SCHED_RR,SCHED_FIFO
prio: 10
}, {
name: "shm"
cpuset: "2"
policy: "SCHED_FIFO"
prio: 10
}
]
choreography_conf {
choreography_processor_num: 8
choreography_affinity: "range"
choreography_cpuset: "2-3"
choreography_processor_policy: "SCHED_FIFO" # policy: SCHED_OTHER,SCHED_RR,SCHED_FIFO
choreography_processor_prio: 10
pool_processor_num: 8
pool_affinity: "range"
pool_cpuset: "7-8"
pool_processor_policy: "SCHED_OTHER"
pool_processor_prio: 0
tasks: [
{
name: "A"
processor: 0
prio: 1
},
{
name: "B"
processor: 0
prio: 2
},
{
name: "C"
processor: 1
prio: 1
},
{
name: "D"
processor: 1
prio: 2
},
{
name: "E"
}
]
}
}
Async产生的协程池里面的执行顺序
系统采用base::BoundedQueue承载所有协程(固定个数,为配置文件第一个group的processor_num),按照先后顺序执行,执行器个数代表并行个数。一个执行体执行完毕才会执行下一个执行体,即使协程进行了yeild也不会让新的执行体开始执行。执行体队列最大值为1000,超过这个值再进行Async会被抛弃。
CreateTask产生的协程的执行顺序
首先看执行体个数和执行器个数,如果执行体小于等于执行器那么大家都会得以立即执行;否则,假设执行器个数为N,那么前N个CreateTask产生的协程会得以立即执行,之后创建的就要看执行优先级了,数值越大的越会优先执行;倘若正在执行的执行体进行了yeild,会有新的执行体得到执行。
经典模式测试
1.调度系统中有个协程池,使用Async调度的任务都是由协程池去执行的,在经典模式中,协程池的属性是由第一个分组属性定义的,包括执行器的个数、CPU亲和性、优先级和调度规则,协程所在分组名称,协程名字格式/internal/task+编号,协程组里面的协程的个数和该分组的执行器个数一样。
2.使用CreateTask生成的协程会根据其名字分配到相应的分组里面,如果在所有分组都没有找到其名字,则将其加入到第一个分组里面。
3.协程优先级取值范围0-19,大于19的都将赋值为19。优先级类型为uint32意为非负整数,一旦出现配置为负数,整个classic_conf都将作废了。执行优先权将从数值大的先开始。协程池里面的由于没有名字也就没有优先权相关信息。
4.如果classic_conf没有配备分组,则会创建分组,分组名称为default_grp,执行器个数从cyber.pb.conf的default_proc_num字段读取,如果没有cyber.pb.conf字段,则执行器个数为2
5.cyber.pb.conf里面的routine_num字段,代表一次性初始化协程上下文的个数,如果将来的协程个数超越了该值之后的上下文要一个一个的申请
编排模式测试
1.协程队列在ChoreographyContext类里面的std::multimap储存着,也是按照优先级从大到小的顺序,这样可以保证优先级数值大的得到先执行的机会
2.choreography_conf里有设置调度器执行器属性的,也有设置协程池执行器属性的配置
3.配置task里多了一个processor执行器的属性,即协程可以指定专属执行器
总结
1.SchedulerClassic 采用了协程池的概念,协程没有绑定到具体的Processor,所有的协程放在全局的优先级队列中,每次从最高优先级的任务开始执行。
2.SchedulerChoreography 采用了本地队列和全局队列相结合的方式,通过"ChoreographyContext"运行本地队列的线程,通过"ClassicContext"来运行全局队列。
3.Processor对协程来说是一个逻辑上的cpu,ProcessorContext实现Processor的运行上下文,通过ProcessorContext来获取协程,休眠或者唤醒,Scheduler调度器实现了协程调度算法。
调度模式存在的共同问题
1.因为Processor每次都会从高优先级的队列开始取任务,假设Processor的数量不够,可能会出现低优先级的协程永远得不到调度的情况。
2.协程的调度没有抢占,也就是说一个协程在执行的过程中,除非主动让出,否则会一直占用Processor,如果Processor在执行低优先级的任务,来了一个高优先级的任务并不能抢占执行