cyber协程
在【Apollo 6.0】 cyber rt是如何使用Reader读取到Writer发送的数据(顶层逻辑) 中,分析到Reader::Init() 的时候,里面使用了协程进行事件处理,那这个协程是怎么实现的呢,这里我们就来分析一下:
auto sched = scheduler::Instance();
Scheduler* Instance() {
Scheduler* obj = instance.load(std::memory_order_acquire);
if (obj == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
obj = instance.load(std::memory_order_relaxed);
if (obj == nullptr) {
//配置文件
std::string policy("classic");
std::string conf("conf/");
conf.append(GlobalData::Instance()->ProcessGroup()).append(".conf");
auto cfg_file = GetAbsolutePath(WorkRoot(), conf);
apollo::cyber::proto::CyberConfig cfg;
//从配置文件取出policy配置
if (PathExists(cfg_file) && GetProtoFromFile(cfg_file, &cfg)) {
policy = cfg.scheduler_conf().policy();
} else {
AWARN << "Scheduler conf named " << cfg_file
<< " not found, use default.";
}
//根据policy来选择使用什么协程模式
if (!policy.compare("classic")) {
obj = new SchedulerClassic();
} else if (!policy.compare("choreography")) {
obj = new SchedulerChoreography();
} else {
AWARN << "Invalid scheduler policy: " << policy;
obj = new SchedulerClassic();
}
instance.store(obj, std::memory_order_release);
}
}
return obj;
}
在Instance()当中,使用到了配置文件来配置协程的模式,那么这个配置文件长什么样呢,这里以example_sched_classic.conf作为讲解:
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,16-23"
processor_policy: "SCHED_OTHER" # policy: SCHED_OTHER,SCHED_RR,SCHED_FIFO
processor_prio: 0
tasks: [
{
name: "E"
prio: 0
}
]
},{
name: "group2"
processor_num: 16
affinity: "1to1"
cpuset: "8-15,24-31"
processor_policy: "SCHED_OTHER"
processor_prio: 0
tasks: [
{
name: "A"
prio: 0
},{
name: "B"
prio: 1
},{
name: "C"
prio: 2
},{
name: "D"
prio: 3
}
]
}
]
}
}
每个配置属性的含义、影响:
- groups:如配置示例,classic策略可以配置多个group,主要为了实现资源隔离、跨numa问题,比如一个进程产生的所有task在0-31号cpu上执行,内核的调度会将线程在0-31号cpu上切换,跨numa节点会给系统带来额外的开销,这里可以通过group将numa节点进行隔离,一个numa节点下的0-7,16-23号cpu划分到一个group中,另外一个numa节点下的8-15,24-31号cpu划分到另一个group,这样既保证了资源利用,也能避免跨numa节点带来的开销。
- process_level_cpuset: 控制mainboard进程使用哪些cpu
- processor_num:创建的processor数,一个processor对于1个线程对应N个协程。
- affinity: 取值为range或者1to1,如第一个group,创建16个线程,在0-7,16-23号cpu上设置亲和性,每个线程都可以在0-7,16-23号核上执行。第二个group中,affinity为1to1,表示16个线程对应8-15,24-31号cpu,每个线程和一个cpu进行亲和性设置,能减少线程在cpu之间切换带来的开销,但是前提是开启的线程数和cpu数必须一致。
- processor_policy和processor_prio: 这两个一般成对出现,processor_policy指设置线程的调度策略,取值为SCHED_FIFO(实时调度策略,先到先服务), SCHED_RR(实时调度策略,时间片轮转), SCHED_OTHER(分时调度策略,为默认策略),对于设置了SCHED_FIFO或者SCHED_RR的线程会更优先的得到cpu执行, 调度模型中设置processor_policy背景:为了保证主链路的任务或者其他一些实时task的优先执行。如果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值越小,分到的时间片越多。
- tasks:这里是对task任务进行配置,name表示task的名字,prio表示任务的优先级,为了提高性能,减小任务队列锁的粒度,调度模型中采用的是多优先级队列,也就是同一优先级的task在同一个队列里面,系统调度时会优先执行优先级高的任务。
我们这里以classic模式的配置文件作为例子讲解,那么我们就先讲解这个模式吧:
SchedulerClassic::SchedulerClassic() {
std::string conf("conf/");
conf.append(GlobalData::Instance()->ProcessGroup()).append(".conf");
auto cfg_file = GetAbsolutePath(WorkRoot(), conf);
apollo::cyber::proto::CyberConfig cfg;
//如果配置文件存在,从文件读取配置信息
if (PathExists(cfg_file) && GetProtoFromFile(cfg_file, &cfg)) {
for (auto& thr : cfg.scheduler_conf().threads()) {
inner_thr_confs_[thr.name()] = thr;
}
//配置主进程的CPU亲和度
if (cfg.scheduler_conf().has_process_level_cpuset()) {
process_level_cpuset_ = cfg.scheduler_conf().process_level_cpuset();
ProcessLevelResourceControl();
}
classic_conf_ = cfg.scheduler_conf().classic_conf();
for (auto& group : classic_conf_.groups()) {
auto& group_name = group.name();
//保存task配置
for (auto task : group.tasks()) {
task.set_group_name(group_name);
cr_confs_[task.name()] = task;
}
}
} else {
// if do not set default_proc_num in scheduler conf
// give a default value
uint32_t proc_num = 2;
//全局的配置文件为cyber.pb.conf
auto& global_conf = GlobalData::Instance()->Config();
if (global_conf.has_scheduler_conf() &&
global_conf.scheduler_conf().has_default_proc_num()) {
proc_num = global_conf.scheduler_conf().default_proc_num();
}
task_pool_size_ = proc_num;
auto sched_group = classic_conf_.add_groups();
sched_group->set_name(DEFAULT_GROUP_NAME);
sched_group->set_processor_num(proc_num);
}
CreateProcessor();
}
可以看到,SchedulerClassic的构造函数就是读取配置文件的配置进行保存,真正对处理线程做配置的在CreateProcessor当中。
void SchedulerClassic::CreateProcessor() {
//根据groups来分配
for (auto& group : classic_conf_.groups()) {
auto& group_name = group.name();
auto proc_num = group.processor_num();
if (task_pool_size_ == 0) {
task_pool_size_ = proc_num;
}
auto& affinity = group.affinity();
auto& processor_policy = group.processor_policy();
auto processor_prio = group.processor_prio();
std::vector<int> cpuset;
ParseCpuset(group.cpuset(), &cpuset);
//一个group分配proc_num个Processor(处理线程)
for (uint32_t i = 0; i < proc_num; i++) {
//创建协程上下文类
auto ctx = std::make_shared<ClassicContext>(group_name);
pctxs_.emplace_back(ctx);
auto proc = std::make_shared<Processor>();
proc->BindContext(ctx);
SetSchedAffinity(proc->Thread(), cpuset, affinity, i);
SetSchedPolicy(proc->Thread(), processor_policy, processor_prio,
proc->Tid());
processors_.emplace_back(proc);
}
}
}
在上面CreateProcessor当中,主要就是下面那个for循环在创建,这里进行分析:
ClassicContext::ClassicContext(const std::string& group_name) {
InitGroup(group_name);
}
void ClassicContext::InitGroup(const std::string& group_name) {
multi_pri_rq_ = &cr_group_[group_name];//保存对应group_name的协程优先级队列
lq_ = &rq_locks_[group_name];//获取对于group_name的优先级锁队列(free lock)
mtx_wrapper_ = &mtx_wq_[group_name];//获取整个group的mutex
cw_ = &cv_wq_[group_name];//获取整个group的条件变量
notify_grp_[group_name] = 0;
current_grp = group_name;
}
ClassicContext中保存的就是对应group的相关变量,其中最重要的就是cr_group_,他的类型为std::unordered_map<std::string, MULTI_PRIO_QUEUE> 展开后:
std::unordered_map< std::string, std::array< std::vector < std::shared_ptr < CRoutine > >
ClassicContext中保存的这些变量是提供Processor使用的:
Processor::Processor() { running_.store(true); }
void Processor::BindContext(const std::shared_ptr<ProcessorContext>& context) {
context_ = context;
std::call_once(thread_flag_,
[this]() { thread_ = std::thread(&Processor::Run, this); });
}
当调用完Processor::BindContext的时候,Processor将会存下传入的ClassicContext。Processor、ClassicContext和cr_group_的关系如下图所示:
Processor中保存着ClassicContext,ClassicContext中存着cr_group_[group_name]的优先级队列,每个优先级都有一个协程队列,当有对应的优先级协程被创建的时候,将会被放入队列当中。而一个group创建了多个Processor,每个Processor对应一个线程,多个Processor从MULTI_PRIO_QUEUE中按优先级获取协程执行。
而当第一次调用Processor::BindContext时,将会创建Processor::Run线程。
void Processor::Run() {
tid_.store(static_cast<int>(syscall(SYS_gettid)));
AINFO << "processor_tid: " << tid_;
snap_shot_->processor_id.store(tid_);
while (cyber_likely(running_.load())) {
if (cyber_likely(context_ != nullptr)) {
//拿出优先级最高的协程
auto croutine = context_->NextRoutine();
if (croutine) {
snap_shot_->execute_start_time.store(cyber::Time::Now().ToNanosecond());
snap_shot_->routine_name = croutine->name();
croutine->Resume();
croutine->Release();
} else {//拿不到则进行wait
snap_shot_->execute_start_time.store(0);
context_->Wait();
}
} else {//如果当前的context_是空的,则进行wait
std::unique_lock<std::mutex> lk(mtx_ctx_);
cv_ctx_.wait_for(lk, std::chrono::milliseconds(10));
}
}
}
在刚创建完Processor::Run() 处理线程的时候,是没有协程被放入的,因此context_->NextRoutine()为空,那么将会调用context_->Wait()进行等待。那谁会将其唤醒呢,别急我们接着往下分析就知道了。
在Reader::Init()之后会调用CreateRoutineFactory在创建协程工程:
template <typename M0, typename F>
RoutineFactory CreateRoutineFactory(
F&& f, const std::shared_ptr<data::DataVisitor<M0>>& dv) {
RoutineFactory factory;
factory.SetDataVisitor(dv);
factory.create_routine = [=]() {
return [=]() {
std::shared_ptr<M0> msg;
for (;;) {
CRoutine::GetCurrentRoutine()->set_state(RoutineState::DATA_WAIT);
if (dv->TryFetch(msg)) {
f(msg);
CRoutine::Yield(RoutineState::READY);
} else {
CRoutine::Yield();
}
}
};
};
return factory;
}
create_routine就是协程的执行函数,这里先不做分析,等到调用到再进行分析
bool Scheduler::CreateTask(std::function<void()>&& func,
const std::string& name,
std::shared_ptr<DataVisitorBase> visitor) {
if (cyber_unlikely(stop_.load())) {
ADEBUG << "scheduler is stoped, cannot create task!";
return false;
}
auto task_id = GlobalData::RegisterTaskName(name);
auto cr = std::make_shared<CRoutine>(func);
cr->set_id(task_id);
cr->set_name(name);
AINFO << "create croutine: " << name;
if (!DispatchTask(cr)) {
return false;
}
if (visitor != nullptr) {
visitor->RegisterNotifyCallback([this, task_id]() {
if (cyber_unlikely(stop_.load())) {
return;
}
this->NotifyProcessor(task_id);
});
}
return true;
}
Scheduler::CreateTask就进行了协程的创建,CRoutine就是协程操作类:
CRoutine::CRoutine(const std::function<void()> &func) : func_(func) {
//第一次进执行
std::call_once(pool_init_flag, [&]() {
uint32_t routine_num = common::GlobalData::Instance()->ComponentNums();
auto &global_conf = common::GlobalData::Instance()->Config();
if (global_conf.has_scheduler_conf() &&
global_conf.scheduler_conf().has_routine_num()) {
routine_num =
std::max(routine_num, global_conf.scheduler_conf().routine_num());
}
//初始化对象池
context_pool.reset(new base::CCObjectPool<RoutineContext>(routine_num));
});
//从对象池当中拿出一个对象
context_ = context_pool->GetObject();
if (context_ == nullptr) {
AWARN << "Maximum routine context number exceeded! Please check "
"[routine_num] in config file.";
context_.reset(new RoutineContext());
}
//创建协程的上下文
MakeContext(CRoutineEntry, this, context_.get());
state_ = RoutineState::READY;
updated_.test_and_set(std::memory_order_release);
}
在第一次构造CRoutine的时候,将会初始化context_pool,CCObjectPool作用就是对象池,这里创建了大小为routine_num的RoutineContext对象池,RoutineContext就是协程的上下文存储数据结构,协程的切换其实就是上下文的切换,因此如果想要在主线程与协程直接进行切换的话,就需要保存好两边的上下文,这样才能做到切换后能恢复。
constexpr size_t STACK_SIZE = 2 * 1024 * 1024;
typedef void (*func)(void*);
struct RoutineContext {
char stack[STACK_SIZE];
char* sp = nullptr;
};
可以看到RoutineContext就是创建一个2M的数组作为协程的栈空间,还有一个栈指针,MakeContext就是构造RoutineContext::stack。
void MakeContext(const func &f1, const void *arg, RoutineContext *ctx) {
ctx->sp = ctx->stack + STACK_SIZE - 2 * sizeof(void *) - REGISTERS_SIZE;
std::memset(ctx->sp, 0, REGISTERS_SIZE);
char *sp = ctx->stack + STACK_SIZE - 2 * sizeof(void *);
*reinterpret_cast<void **>(sp) = reinterpret_cast<void *>(f1);
sp -= sizeof(void *);
*reinterpret_cast<void **>(sp) = const_cast<void *>(arg);
}
调用完MakeContext之后,协程的栈将会变成下面这个样子,寄存器相关知识可以参考x86-64中的寄存器:
回调Scheduler::CreateTask继续分析,构造完CRoutine之后,调用了DispatchTask分配任务:
bool SchedulerClassic::DispatchTask(const std::shared_ptr<CRoutine>& cr) {
// we use multi-key mutex to prevent race condition
// when del && add cr with same crid
MutexWrapper* wrapper = nullptr;
//获取协程的mutex
if (!id_map_mutex_.Get(cr->id(), &wrapper)) {
{
std::lock_guard<std::mutex> wl_lg(cr_wl_mtx_);
if (!id_map_mutex_.Get(cr->id(), &wrapper)) {
wrapper = new MutexWrapper();
id_map_mutex_.Set(cr->id(), wrapper);
}
}
}
std::lock_guard<std::mutex> lg(wrapper->Mutex());
{
WriteLockGuard<AtomicRWLock> lk(id_cr_lock_);
if (id_cr_.find(cr->id()) != id_cr_.end()) {
return false;
}
//保存协程
id_cr_[cr->id()] = cr;
}
//查看这个协程有没有配置信息,cr_confs_就是在构造SchedulerClassic时
//保存配置文件里的配置项信息
if (cr_confs_.find(cr->name()) != cr_confs_.end()) {
ClassicTask task = cr_confs_[cr->name()];
cr->set_priority(task.prio());
cr->set_group_name(task.group_name());
} else {
// croutine that not exist in conf
cr->set_group_name(classic_conf_.groups(0).name());
}
if (cr->priority() >= MAX_PRIO) {
AWARN << cr->name() << " prio is greater than MAX_PRIO[ << " << MAX_PRIO
<< "].";
cr->set_priority(MAX_PRIO - 1);
}
// Enqueue task.
{
WriteLockGuard<AtomicRWLock> lk(
ClassicContext::rq_locks_[cr->group_name()].at(cr->priority()));
//将协程放入到对应group的对应优先级的队列当中
ClassicContext::cr_group_[cr->group_name()]
.at(cr->priority())
.emplace_back(cr);
}
ClassicContext::Notify(cr->group_name());
return true;
}
将协程任务放入到对应group的对应优先级的队列当中之后,调用了Notify来唤醒该group下的processor
void ClassicContext::Notify(const std::string& group_name) {
(&mtx_wq_[group_name])->Mutex().lock();
notify_grp_[group_name]++;
(&mtx_wq_[group_name])->Mutex().unlock();
cv_wq_[group_name].Cv().notify_one();
}
Notify调用了notify_one来环境等待该条件变量的线程,也就是在Processor::Run中的ClassicContext::Wait()
void ClassicContext::Wait() {
std::unique_lock<std::mutex> lk(mtx_wrapper_->Mutex());
//当notify_grp_[current_grp]大于0 或者 阻塞1000ms 或者 被条件变量唤醒,将会退出阻塞状态
cw_->Cv().wait_for(lk, std::chrono::milliseconds(1000),
[&]() { return notify_grp_[current_grp] > 0; });
if (notify_grp_[current_grp] > 0) {
notify_grp_[current_grp]--;
}
}
从Wait的实现可以看出,在Processor::Run中调用完Wait不是一直阻塞的,如果没被唤醒的话,再1000ms之后将会再次执行。当将协程放入cr_group_[group_name]之后,Processor::Run被唤醒执行,这时从ClassicContext::NextRoutine中就可以拿出协程了:
std::shared_ptr<CRoutine> ClassicContext::NextRoutine() {
if (cyber_unlikely(stop_.load())) {
return nullptr;
}
//从最高优先级开始取
for (int i = MAX_PRIO - 1; i >= 0; --i) {
ReadLockGuard<AtomicRWLock> lk(lq_->at(i));
for (auto& cr : multi_pri_rq_->at(i)) {
if (!cr->Acquire()) {
continue;
}
if (cr->UpdateState() == RoutineState::READY) {
return cr;
}
cr->Release();
}
}
return nullptr;
}
当有准备好的协程的时候,将会返回该协程,之后执行 CRoutine::Resume()
RoutineState CRoutine::Resume() {
if (cyber_unlikely(force_stop_)) {
state_ = RoutineState::FINISHED;
return state_;
}
if (cyber_unlikely(state_ != RoutineState::READY)) {
AERROR << "Invalid Routine State!";
return state_;
}
current_routine_ = this;
SwapContext(GetMainStack(), GetStack());
current_routine_ = nullptr;
return state_;
}
从CRoutine::Resume的代码来看,没有执行到CreateRoutineFactory时创建的执行函数啊,那到底是怎么执行的呢,其实执行的方法就是在SwapContext当中:
inline void SwapContext(char** src_sp, char** dest_sp) {
ctx_swap(reinterpret_cast<void**>(src_sp), reinterpret_cast<void**>(dest_sp));
}
extern "C" {
extern void ctx_swap(void**, void**) asm("ctx_swap");
};
asm(“ctx_swap”)是执行了汇编实现的ctx_swap函数,这个函数是在swap_x86_64.S中实现的:
.globl ctx_swap
.type ctx_swap, @function
ctx_swap:
pushq %rdi
pushq %r12
pushq %r13
pushq %r14
pushq %r15
pushq %rbx
pushq %rbp
movq %rsp, (%rdi)
movq (%rsi), %rsp
popq %rbp
popq %rbx
popq %r15
popq %r14
popq %r13
popq %r12
popq %rdi
ret
pushq:将后面的寄存器数据放入到sp所指向的栈当中。
movq:将后面的值赋值给前面的
popq:sp所指向的栈空间从取出对应的数据放入后面的寄存器当中。
ret: 出栈,回到栈指针,执行rip指向的指令
当执行完ret之前的执行两个栈空间和寄存器的关系为:
这是sp指向的协程的栈空间,因此根据MakeContext构造的栈空间,ret将会执行rip指向的指令,也就是MakeContext传入的f1(CRoutineEntry),参数为传入的arg(*CRoutine):
void CRoutineEntry(void *arg) {
CRoutine *r = static_cast<CRoutine *>(arg);
r->Run();
CRoutine::Yield(RoutineState::FINISHED);
}
CRoutine::Run() { func_(); }
这个func_就是构造时传入的,也就是:
factory.create_routine = [=]() {
return [=]() {
std::shared_ptr<M0> msg;
for (;;) {
CRoutine::GetCurrentRoutine()->set_state(RoutineState::DATA_WAIT);
if (dv->TryFetch(msg)) {
f(msg);
CRoutine::Yield(RoutineState::READY);
} else {
CRoutine::Yield();
}
}
};
};
CRoutine::Resume()是从主线程空间切换到协程空间运行协程函数,而CRoutine::Yield()就是恢复到线程空间
inline void CRoutine::Yield(const RoutineState &state) {
auto routine = GetCurrentRoutine();
routine->set_state(state);
SwapContext(GetCurrentRoutine()->GetStack(), GetMainStack());
}
总的来说,协程就是通过栈的切换实现的:
执行协程:
- 通过先配置协程栈,将执行函数和参数放到对应的寄存器位置
- 将寄存器保存到主线程栈当中,保存主线程栈地址
- 将sp指向协程栈
- 从协程栈中读取原先配置的数据到寄存器当中
- 执行对应寄存器数据,实现执行协程函数的目的
协程退出:
- 将寄存器的数据放回到协程栈当中
- sp指向线程栈
- 从线程栈读取原先的寄存器数据到寄存器当中,恢复现场
总结下SchedulerClassic:
- SchedulerClassic有一个cr_group的map,其定义为:std::unordered_map<std::string, std::array<std::vector<std::shared_ptr>, 20>>,每个group_name下有20个优先级。每个优先级下面有个存放协程的vector。ClassicContext根据构造时传入的group_name从cr_group[group_name] 拿出对应的array。
- Proseccor在调用完Processor::BindContext之后将会保存传入的ClassicContext,并且启动Run线程,Run中调用ClassicContext::NextRoutine在cr_group[group_name]中从最高优先级开始从vector的拿出CRoutine,然后判断该CRoutine是否准备就绪, 如果就绪,则通过寄存器保存和栈空间切换这种方式来执行协程函数。否则调用ClassicContext::Wait等待被ClassicContext::Notify唤醒 或者1S超时返回。
- 总的来说就是SchedulerClassic模式使用的是一个全局的协程队列cr_group来进行管理。
在cyber中协程除了SchedulerClassic模式还有SchedulerChoreography模式,我们也进行分析:
Scheduler* Instance()
if (!policy.compare("choreography"))
obj = new SchedulerChoreography();
...//中间部分跟SchedulerClassic一样,都是对配置文件的配置保存
//主要的不同就是这个函数
CreateProcessor()
void SchedulerChoreography::CreateProcessor() {
//创建ChoreographyContext
for (uint32_t i = 0; i < proc_num_; i++) {
auto proc = std::make_shared<Processor>();
auto ctx = std::make_shared<ChoreographyContext>();
proc->BindContext(ctx);
SetSchedAffinity(proc->Thread(), choreography_cpuset_,
choreography_affinity_, i);
SetSchedPolicy(proc->Thread(), choreography_processor_policy_,
choreography_processor_prio_, proc->Tid());
pctxs_.emplace_back(ctx);
processors_.emplace_back(proc);
}
//创建ClassicContext,使用默认的组名DEFAULT_GROUP_NAME = "default_grp"
for (uint32_t i = 0; i < task_pool_size_; i++) {
auto proc = std::make_shared<Processor>();
auto ctx = std::make_shared<ClassicContext>();
proc->BindContext(ctx);
SetSchedAffinity(proc->Thread(), pool_cpuset_, pool_affinity_, i);
SetSchedPolicy(proc->Thread(), pool_processor_policy_, pool_processor_prio_,
proc->Tid());
pctxs_.emplace_back(ctx);
processors_.emplace_back(proc);
}
}
SchedulerChoreography::CreateProcessor这里创建了两个Context,下面那个就是上面分析的ClassicContext这里使用的是cr_group_[“default_grp”]作为全局队列,而ChoreographyContext则是使用的优先级本地队列。那SchedulerChoreography模式是怎么分配任务到这两个队列当中的呢,这就需要看SchedulerChoreography::DispatchTask
bool SchedulerChoreography::DispatchTask(const std::shared_ptr<CRoutine>& cr) {
// we use multi-key mutex to prevent race condition
// when del && add cr with same crid
MutexWrapper* wrapper = nullptr;
if (!id_map_mutex_.Get(cr->id(), &wrapper)) {
{
std::lock_guard<std::mutex> wl_lg(cr_wl_mtx_);
if (!id_map_mutex_.Get(cr->id(), &wrapper)) {
wrapper = new MutexWrapper();
id_map_mutex_.Set(cr->id(), wrapper);
}
}
}
std::lock_guard<std::mutex> lg(wrapper->Mutex());
// Assign sched cfg to tasks according to configuration.
if (cr_confs_.find(cr->name()) != cr_confs_.end()) {
ChoreographyTask taskconf = cr_confs_[cr->name()];
cr->set_priority(taskconf.prio());
if (taskconf.has_processor()) {
cr->set_processor_id(taskconf.processor());
}
}
{
WriteLockGuard<AtomicRWLock> lk(id_cr_lock_);
if (id_cr_.find(cr->id()) != id_cr_.end()) {
return false;
}
id_cr_[cr->id()] = cr;
}
// Enqueue task.
//通过配置文件传入,或者默认值为-1,pid为uint32_t,因此-1将会被转为一个很大的值,
//所以如果不设置processor_id的话,默认使用ClassicContext
uint32_t pid = cr->processor_id();
//在CreateProcessor可以看到前proc_num_存放的是ChoreographyContext,
//因此这个分支使用的就是ChoreographyContext
if (pid < proc_num_) {
// Enqueue task to Choreo Policy.
// Enqueue的实现就是 cr_queue_.emplace(cr->priority(), cr);
static_cast<ChoreographyContext*>(pctxs_[pid].get())->Enqueue(cr);
} else {
// Check if task prio is reasonable.
if (cr->priority() >= MAX_PRIO) {
AWARN << cr->name() << " prio great than MAX_PRIO.";
cr->set_priority(MAX_PRIO - 1);
}
cr->set_group_name(DEFAULT_GROUP_NAME);
// Enqueue task to pool runqueue.
{
WriteLockGuard<AtomicRWLock> lk(
ClassicContext::rq_locks_[DEFAULT_GROUP_NAME].at(cr->priority()));
ClassicContext::cr_group_[DEFAULT_GROUP_NAME]
.at(cr->priority())
.emplace_back(cr);
}
}
return true;
}
可以看到SchedulerChoreography模式是通过pid来分配任务的。
总结
- SchedulerClassic 使用了对象池,创建RoutineContext对象池,可以反复使用,避免创建过多的协程栈空间。Croutine与Processor没有绑定的关系,CRoutine被放到了全局的优先级队列当中,Processor从对应的group中取出优先级队列,根据优先级从高到底执行任务。
- SchedulerChoreography 则使用了全局队列和本地队列结合的方式,ChoreographyContext保存本地队列,ClassicContext保存全局队列
- Processor中创建了一个线程,对于协程来说就是一个CPU,Processor通过ChoreographyContext和ClassicContext来获取获取CRoutine,然后通过CRoutine来切换上下文,执行协程。