写点什么

一个 cpp 协程库的前世今生(十八)空闲与等待

作者:SkyFire
  • 2022 年 1 月 11 日
  • 本文字数:3498 字

    阅读完需:约 11 分钟

一个cpp协程库的前世今生(十八)空闲与等待

为了防止大家找不到代码,还是先贴一下项目链接:


GitHub - skyfireitdiy/cocpp at cocpp-0.1.0


本文我们将介绍运行环境在没有协程调度时如何进行休眠等待。


此处的实现涉及到一个重要的类 co_sleep_controller

co_sleep_controller

接口

该类的接口定义位于 include/cocpp/core/co_sleep_controller.h:


class co_sleep_controller final{private:    std::mutex              mu__;      // 互斥锁    std::condition_variable cond__;    // 条件变量    std::function<bool()>   checker__; // 条件检查函数public:    co_sleep_controller(std::function<bool()> checker); // 构造函数    void        wake_up();                              // 唤醒    void        sleep_if_need();                        // 如果需要睡眠则睡眠    std::mutex& sleep_lock();                           // 获取睡眠锁};
复制代码


先对接口做一下介绍:


  • 构造函数:接收一个回调函数,用于判断是否需要睡眠,如果需要睡眠返回 true

  • sleep_if_need:调用此函数进行睡眠,如果不需要睡眠,自然如果会直接返回

  • wake_up:唤醒正在睡眠的协程,如果协程没有睡眠,则什么事都不做

  • sleep_lock:返回与条件变量关联的锁(这个接口设计的并不好,会向外界暴露内部数据)


然后我们再对成员变量做一些解释:


  • mu__:与条件变量关联的互斥量,同时也是外部检测条件的保护锁(因此需要 sleep_lock 来暴露此成员变量)

  • cond__:用来控制睡眠的条件变量

  • checker__:用来检测是否需要睡眠的回调函数


接下来我们来看这个类的实现。

实现

实现的代码位于 source/cocpp/core/co_sleep_controller.cpp:


void co_sleep_controller::wake_up(){    std::lock_guard<std::mutex> lock(mu__);    cond__.notify_one();}
void co_sleep_controller::sleep_if_need(){ std::unique_lock<std::mutex> lock(mu__); if (checker__()) { cond__.wait(lock); }}
co_sleep_controller::co_sleep_controller(std::function<bool()> checker) : checker__(checker){}
std::mutex& co_sleep_controller::sleep_lock(){ return mu__;}
复制代码


重点看一下 sleep_if_need 和 wake_up

sleep_if_need

该函数的实现,先获取互斥量,然后判断是否需要睡眠,如果需要,就使用条件变量来等待。

wake_up

该函数唤醒等待的条件变量。


接下来我们来看一下如何使用 co_sleep_controller。

使用 co_sleep_controller

在每个 env 中有个睡眠控制器,作为成员变量定义在 include/cocpp/core/co_env.h:


co_sleep_controller  sleep_controller__; // 睡眠控制器
复制代码


初始化位于 env 在构造函数中(source/cocpp/core/co_env.cpp):


co_env::co_env(co_stack* shared_stack, co_ctx* idle_ctx, bool create_new_thread)    : sleep_controller__([this] { return need_sleep__(); })    , shared_stack__(shared_stack)    , idle_ctx__(idle_ctx){    // ...}
复制代码


可以看到,构造函数传入的是 env 的 need_sleep__函数。

need_sleep__

need_sleep__的实现很简单(source/cocpp/core/co_env.cpp):


bool co_env::need_sleep__(){    return !can_schedule__() && state() != co_env_state::destorying;}
复制代码


当当前的 env 不处于正在被销毁状态并且也没有 ctx 需要调度的时候,就认为需要睡眠了。


那么什么时候会去检测呢?

start_schedule_routine__

这个函数是每个 env 关联的调度线程的执行函数,定义为(source/cocpp/core/co_env.cpp):



void co_env::start_schedule_routine__(){ co_interrupt_closer interrupt_closer; schedule_thread_tid__ = gettid(); schedule_started().pub(); reset_flag(CO_ENV_FLAG_NO_SCHE_THREAD); set_state(co_env_state::idle); while (state() != co_env_state::destorying) { schedule_switch();
// 切换回来检测是否需要执行共享栈切换 if (shared_stack_switch_info__.need_switch) { continue; }
set_state(co_env_state::idle); // 切换到idle协程,说明空闲了
sleep_if_need(); }
remove_all_ctx__(); task_finished().pub(); current_env__ = nullptr;}
复制代码


当我们有 ctx 需要调度的时候,调度线程会在不同的 ctx 之间切换,不会回到这个函数中来(共享栈切换除外,共享栈的切换需要借助空闲协程,但是这里暂时不讨论,留在后面共享栈部分再讨论)。因此,如果从 schedule_switch 返回到此处,说明没有协程需要调度了,此刻需要将状态设置为空闲,并检测是否需要睡眠。

唤醒

接下来我们来看一下哪些条件下需要唤醒。

有新的协程加入

当前运行环境正在休眠,当有新的协程加入时需要唤醒他来调度新的协程。实现位于函数 co_env::move_ctx_to_here(source/cocpp/core/co_env.cpp):


void co_env::move_ctx_to_here(co_ctx* ctx){    assert(ctx != nullptr);    assert(state() != co_env_state::created && state() != co_env_state::destorying);
ctx->set_env(this);
{ std::lock_guard<co_spinlock> lock(mu_normal_ctx__); all_normal_ctx__[ctx->priority()].push_back(ctx); }
update_min_priority__(ctx->priority()); set_state(co_env_state::busy);
wake_up(); ctx_received().pub(ctx);}
复制代码

停止调度

当整个运行环境需要停止调度时,要将 env 从休眠状态唤醒来执行一些清理操作。实现位于 co_env::stop_schedule(source/cocpp/core/co_env.cpp):


void co_env::stop_schedule(){    if (!test_flag(CO_ENV_FLAG_NO_SCHE_THREAD))    {        set_state(co_env_state::destorying);    }
wake_up();
schedule_stopped().pub();}
复制代码

协程退出资源等待状态

当某个协程退出了资源等待状态,说明它又可以被调度了。此时需要唤醒与他关联的 env。实现位于函数 co_ctx::leave_wait_resource_state(source/cocpp/core/co_ctx.cpp):


void co_ctx::leave_wait_resource_state(){    reset_flag(CO_CTX_FLAG_WAITING);    std::lock_guard<co_spinlock> lock(env_lock__);    env__->ctx_leave_wait_state(this);    env__->wake_up();    wait_resource_state_leaved().pub();}
复制代码

需要保护的状态

前面我们提到有一个暴露睡眠控制器内部互斥量的接口 sleep_lock,这里我们来看一下,哪些地方要用这个互斥量来保护。

ctx 状态和标识的改变

ctx 状态的改变需要使用睡眠控制器的锁保护(source/cocpp/core/co_ctx.cpp):


void co_ctx::set_state(const co_state& state){    // TODO 此处可以优化    std::scoped_lock lock(env_lock__);    if (env__ != nullptr)    {        std::scoped_lock lock(env__->sleep_lock());        state_manager__.set_state(state);        state_set().pub(state);        return;    }    state_manager__.set_state(state);    state_set().pub(state);}
void co_ctx::set_flag(size_t flag){ // TODO 此处可以优化 std::scoped_lock lock(env_lock__); if (env__ != nullptr) { std::scoped_lock lock(env__->sleep_lock()); flag_manager__.set_flag(flag); flag_set().pub(flag); return; } flag_manager__.set_flag(flag); flag_set().pub(flag);}
void co_ctx::reset_flag(size_t flag){ // TODO 此处可以优化 std::scoped_lock lock(env_lock__); if (env__ != nullptr) { std::scoped_lock lock(env__->sleep_lock()); flag_manager__.reset_flag(flag); flag_reset().pub(flag); return; } flag_manager__.reset_flag(flag); flag_reset().pub(flag);}
复制代码


注意此处标记的“可以优化”,这里将所有的状态都进行了保护,实际上仅需要保护某几个可以影响到 co_env::need_sleep__结果的状态即可。

env 状态的改变

对 env 状态的改变也需要保护(source/cocpp/core/co_env.cpp)。


void co_env::set_state(const co_env_state& state){    std::scoped_lock lock(sleep_lock());    state_manager__.set_state(state);}
复制代码


总的来说,凡是影响到 env::need_sleep__结果的操作都需要睡眠控制器中互斥量的保护。

总结

本文介绍了协程休眠的原理,实际上是休眠调度线程,使用条件变量使调度线程让出 CPU,避免空转。另外还介绍了进入睡眠的时机,唤醒的时机,以及哪些影响睡眠的操作,需要同步互斥量来保护。

发布于: 刚刚阅读数: 2
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018.10.13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(十八)空闲与等待