【Apollo 6.0】cyber rt协程实现

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());
}

总的来说,协程就是通过栈的切换实现的:
执行协程:

  1. 通过先配置协程栈,将执行函数和参数放到对应的寄存器位置
  2. 将寄存器保存到主线程栈当中,保存主线程栈地址
  3. 将sp指向协程栈
  4. 从协程栈中读取原先配置的数据到寄存器当中
  5. 执行对应寄存器数据,实现执行协程函数的目的

协程退出:

  1. 将寄存器的数据放回到协程栈当中
  2. sp指向线程栈
  3. 从线程栈读取原先的寄存器数据到寄存器当中,恢复现场

总结下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来切换上下文,执行协程。
  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值