为了防止大家找不到代码,还是先贴一下项目链接:
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,避免空转。另外还介绍了进入睡眠的时机,唤醒的时机,以及哪些影响睡眠的操作,需要同步互斥量来保护。
评论