为了防止大家找不到代码,还是先贴一下项目链接:
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:返回与条件变量关联的锁(这个接口设计的并不好,会向外界暴露内部数据)
然后我们再对成员变量做一些解释:
接下来我们来看这个类的实现。
实现
实现的代码位于 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,避免空转。另外还介绍了进入睡眠的时机,唤醒的时机,以及哪些影响睡眠的操作,需要同步互斥量来保护。
评论