为了防止大家找不到代码,还是先贴一下项目链接:
GitHub - skyfireitdiy/cocpp at cocpp-0.1.0
前面介绍了三大基础组件的基本信息,接下来两篇文章介绍一下协程上下文 ctx 和协程执行环境 env 的一些标志和状态以及他们是如何转换的。
本文介绍 ctx 的标志和状态含义及转换场景。
标识
标识位定义在 include/core/co_define.h 中:
constexpr int CO_CTX_FLAG_WAITING = 0; // 等待
constexpr int CO_CTX_FLAG_LOCKED = 1; // 被co对象持有,暂时不能销毁
constexpr int CO_CTX_FLAG_BIND = 2; // 绑定env,不可移动
constexpr int CO_CTX_FLAG_IDLE = 3; // idle ctx
constexpr int CO_CTX_FLAG_SHARED_STACK = 4; // 共享栈
constexpr int CO_CTX_FLAG_MAX = 8; // 最大标志位
复制代码
接下来一一介绍这些标识位的含义及转换关系。
CO_CTX_FLAG_WAITING 等待标识
此标志的含义是当前 ctx 正在等待资源,目前这个资源只有锁,可能是互斥锁、读写锁等。要等待的资源记录在 ctx 的成员 wait_data__中,类型为 co_ctx_wait_data,定义在 include/cocpp/core/co_type.h 中:
struct co_ctx_wait_data
{
co_spinlock mu { co_spinlock::lock_type::in_thread }; // 互斥锁
int type; // 等待类型
void* resource; // 等待资源
};
复制代码
成员 mu 是保护此结构使用的。
type 的取值定义在 include/cocpp/core/co_define.h 中:
constexpr int CO_RC_TYPE_MUTEX = 0; // 互斥锁
constexpr int CO_RC_TYPE_RECURSIVE_MUTEX = 1; // 递归互斥锁
constexpr int CO_RC_TYPE_SHARED_MUTEX = 2; // 共享互斥锁
复制代码
设置时机
由于等待互斥锁、递归互斥锁或者共享互斥锁而导致协程阻塞的时候,会设置 CO_CTX_FLAG_WAITING 标识(细节在后面锁的相关章节再说明),设置的函数见 source/cocpp/core/co_ctx.cpp:
void co_ctx::enter_wait_resource_state(int rc_type, void* rc)
{
std::lock_guard<co_spinlock> lock(wait_data__.mu);
wait_data__.type = rc_type;
wait_data__.resource = rc;
set_flag(CO_CTX_FLAG_WAITING);
std::lock_guard<co_spinlock> env_lock(env_lock__);
env__->ctx_enter_wait_state(this);
wait_resource_state_entered().pub(rc_type, rc);
}
复制代码
函数接受资源类型与资源指针,设置 CO_CTX_FLAG_WAITING 标识。
何时检测
何时检测的问题其实就是这个标识有什么作用。
这个标识一旦设置就说明此 ctx 不需要没调度了(标识存在说明资源不满足继续运行的条件),因此在判断 ctx 是否可以调度的时候会检测此标识(source/cocpp/core/co_ctx.cpp):
bool co_ctx::can_schedule() const
{
return state() != co_state::finished && !test_flag(CO_CTX_FLAG_WAITING);
}
复制代码
清除时机
当等待的资源满足条件后,会将此标识删除(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();
}
复制代码
注意退出资源等待状态的时候函数是不需要参数的,因为一个 ctx 在某个时间只会被某一个资源阻塞,不会同时等待多个资源。
wait_data__是否必要
从上面的描述来看,wait_data__这个结构其实一直没有被使用过,只是在进入等待状态的时候赋了一下值,那么这个字段是否有存在的必要呢?我的回答是,当前阶段是需要的,主要是由于调试需要,当出现协程卡死的时候,我需要知道它等待的是哪个资源,这个资源当前被谁持有着,是否构成了死锁。这些信息都可以在调试器中看到,因此目前这个字段还是需要的,可能后面的某个版本会将这个字段去除吧。
CO_CTX_FLAG_LOCKED 锁定标识
此标识被称为锁定标识,其实也可称为禁止销毁标识。标识的含义是当前 ctx 还在被使用,不能被销毁。
当一个协程运行结束时,如果没有被设置这个标识,就会被调度器销毁掉,此时 ctx 的生命周期走向终点。因此此标志是标记了一个 ctx 是否可以被销毁(source/cocpp/core/co_ctx.cpp):
bool co_ctx::can_destroy() const
{
return !test_flag(CO_CTX_FLAG_LOCKED);
}
复制代码
设置时机
那么,什么时候会设置这个标识呢?
目前是除了 idle 协程外,所有新增的协程都会设置这个标识,因为新增的协程都会返回一些上层封装的 co 对象(具体在 include/cocpp/interface/co.h 中,后面有专门的章节介绍用户接口)
检测时机
每次调度都会检测所有协程是否满足被销毁条件,如下(source/cocpp/core/co_env.cpp):
void co_env::remove_detached_ctx__()
{
auto curr = current_ctx();
auto all_ctx = all_scheduleable_ctx__();
for (auto& ctx : all_ctx)
{
// 注意:此处不能删除当前的ctx,如果删除了,switch_to的当前上下文就没地方保存了
if (ctx->state() == co_state::finished && ctx->can_destroy() && ctx != curr)
{
remove_ctx(ctx);
}
}
}
void co_env::schedule_switch()
{
lock_schedule();
co_interrupt_closer interrupt_closer;
remove_detached_ctx__();
if (shared_stack_switch_info__.need_switch)
{
switch_shared_stack_ctx__();
}
else
{
switch_normal_ctx__();
}
unlock_schedule();
}
复制代码
schedule_switch 是切换函数,每次切换都会检测是否需要删除可销毁的 ctx,而判断标准就是 ctx 当前已完成并且未设置 CO_CTX_FLAG_LOCKED 标识。
清除时机
协程创建的时候,会向用户返回一个 co 对象,只要这个对象存在,就说明这个 ctx 就有被访问的可能,就不能被销毁掉,所以,当返回的 co 对象生命周期结束的时候或者用户显式设置不再跟踪 ctx 的时候,就会清除 CO_CTX_FLAG_LOCKED 标识,此 ctx 才会被调度框架销毁。
source/cocpp/core/co_ctx.cpp:
void co_ctx::unlock_destroy()
{
reset_flag(CO_CTX_FLAG_LOCKED);
unlocked_destroy().pub();
}
复制代码
source/cocpp/interface/co.cpp:
void co::detach()
{
if (ctx__ == nullptr)
{
return;
}
ctx__->unlock_destroy();
ctx__ = nullptr;
}
co::~co()
{
detach();
}
复制代码
CO_CTX_FLAG_BIND 绑定标识
此标识比较简单,是标记此 ctx 与一个指定的 env 绑定,让调度框架在做协程迁移的时候不要迁移此协程。
设置时机
当创建协程的时候,可以传入一些配置参数,其中有一个参数是 bind_env,见 include/cocpp/core/co_ctx_config.h
struct co_ctx_config
{
size_t stack_size { CO_DEFAULT_STACK_SIZE }; // 栈大小
std::string name { "__unknown__" }; // 协程名称
size_t priority { 99 }; // 优先级 (0~99,数字越小,优先级约高)
co_env* bind_env { nullptr }; // 指定env
bool shared_stack { false }; // 共享栈
};
复制代码
当指定了 env 的时候,创建 ctx 的时候,就会为其设置上该标识(source/cocpp/core/co_ctx_factory.cpp):
co_ctx* co_ctx_factory ::create_ctx(const co_ctx_config& config, std::function<void(co_any&)> entry)
{
auto ret = ctx_pool__.create_obj(config.shared_stack ? nullptr : co_stack_factory::instance()->create_stack(config.stack_size), config, entry);
assert(ret != nullptr);
if (config.bind_env != nullptr)
{
ret->set_flag(CO_CTX_FLAG_BIND);
}
if (config.shared_stack)
{
ret->set_flag(CO_CTX_FLAG_SHARED_STACK);
}
return ret;
}
复制代码
检测时机
此标志用来作为协程是否可以迁移的一个判断标准(source/cocpp/core/co_ctx.cpp):
bool co_ctx::can_move() const
{
return !(state() == co_state::running || state() == co_state::finished || test_flag(CO_CTX_FLAG_BIND) || test_flag(CO_CTX_FLAG_SHARED_STACK) || test_flag(CO_CTX_FLAG_SWITCHING));
}
复制代码
管理器在执行迁移或者协程偷取的时候会将 env 中可迁移的协程取出来,设置了这个标识的协程就会被过滤掉。具体见 source/cocpp/core/co_env.cpp 中的 take_all_movable_ctx 和 take_one_movable_ctx 函数。
清除时机
此标识不会被清除。
CO_CTX_FLAG_IDLE 空闲协程
此标识含义是当前协程是个空闲协程。每个 env 都会有一个空闲协程,该协程在没有可调度的协程的时候才会运行。
设置时机
该标志在空闲协程被创建的时候设置(source/cocpp/core/co_env_factory.cpp):
co_ctx* co_env_factory::create_idle_ctx__()
{
co_ctx_config config;
config.name = "idle";
config.stack_size = 0;
config.priority = CO_IDLE_CTX_PRIORITY;
auto ret = co_ctx_factory::instance()->create_ctx(config, nullptr);
ret->set_flag(CO_CTX_FLAG_IDLE);
return ret;
}
复制代码
此函数会被每个 env 创建的时候调用,(见函数 co_env_factory::create_env 和 co_env_factory::create_env_from_this_thread)。
检测时机
此标识仅在对 idle 协程需要特殊处理的时候才需要检测,如优先级设置(source/cocpp/core/co_ctx.cpp):
void co_ctx::set_priority(int priority)
{
if (test_flag(CO_CTX_FLAG_IDLE))
{
return;
}
// ...
}
复制代码
清除时机
此标识不会被清除。
CO_CTX_FLAG_SHARED_STACK 共享栈标识
此标识标识当前协程是否是与其他协程共享一个栈空间的。关于共享栈,此处可以简要介绍一下,其示意图如下:
在共享栈的设计下,每个执行环境会开辟一个共享栈,当一个协程被切出的时候,共享栈中的内容会保存到其自身的栈空间,但是不需要保存所有内容,仅需要保存有效的那些内容,当共享栈协程被调度的时候,再从自身的栈空间拷贝到共享栈的空间中。以此来节省内存使用。但是凡事必定有两面性,此方法节省了内存使用,但是在每次切换的时候需要付出拷贝有效栈空间的时间成本,是一种时间换空间的设计。
以上图举例说明,假设共享栈的大小是 8MB,co1 有效栈空间为 8KB,co2 为 2KB,co3 为 6KB,co4 为 4KB,那么使用共享栈的情况需要消耗内存的量为:8192KB+8KB+2KB+6KB+4KB=8312KB,而如果不使用共享栈,我们不得不为每个 co 开辟一块 8MB 的空间才能与使用共享栈时等价的效果。
有的读者可能会问了,前面不是提过可以为协程设置栈空间大小吗,将 co1 的栈空间设置为 8KB、co2 的栈空间设置为 2KB,co3 设置为 6KB,co4 设置为 4KB 不就好了,这样只占 20KB 内存呀。确实可以这么做,但是前提是因为我们知道该协程运行时最大就占用那么大的栈空间,然而事实上,在程序实际运行起来之前,我们通常是无法预估出需要多少栈空间的,递归函数的不同深度,不同分支的不同调用链都会影响栈空间的最大值,因此,一开始预留足够的栈空间总是一个有效的解决办法。
设置时机
当创建协程传入了共享栈设置的时候就会设置此标识,并且不会为 ctx 分配单独的栈空间(source/cocpp/core/co_ctx_factory.cpp)。
co_ctx* co_ctx_factory ::create_ctx(const co_ctx_config& config, std::function<void(co_any&)> entry)
{
auto ret = ctx_pool__.create_obj(config.shared_stack ? nullptr : co_stack_factory::instance()->create_stack(config.stack_size), config, entry);
assert(ret != nullptr);
if (config.bind_env != nullptr)
{
ret->set_flag(CO_CTX_FLAG_BIND);
}
if (config.shared_stack)
{
ret->set_flag(CO_CTX_FLAG_SHARED_STACK);
}
return ret;
}
复制代码
检测时机
void init_ctx(co_stack* shared_stack, co_ctx* ctx)
{
auto context = get_sigcontext_64(ctx);
co_stack* stack = ctx->stack();
if (ctx->test_flag(CO_CTX_FLAG_SHARED_STACK))
{
stack = shared_stack;
}
auto config = ctx->config();
CO_SETREG(context, sp, stack->stack_top());
CO_SETREG(context, bp, stack->stack_top());
CO_SETREG(context, ip, &co_ctx::real_entry);
CO_SETREG(context, di, ctx);
}
复制代码
共享栈不需要分配独立的栈空间,因此需要将 env 的共享栈指针设置给它。
当需要发生切换的时候,会检测当前以及下一个协程是否是共享栈协程(source/cocpp/core/co_env.cpp):
bool co_env::prepare_to_switch(co_ctx*& from, co_ctx*& to)
{
// ...
if (curr->test_flag(CO_CTX_FLAG_SHARED_STACK) || next->test_flag(CO_CTX_FLAG_SHARED_STACK))
{
shared_stack_switch_info__.from = curr;
shared_stack_switch_info__.to = next;
shared_stack_switch_info__.need_switch = true;
// CO_DEBUG("prepare from:%p to:%p", shared_stack_switch_context__.from, shared_stack_switch_context__.to);
if (curr == idle_ctx__)
{
return false;
}
next = idle_ctx__;
// CO_DEBUG("from %p to idle %p", curr, idle_ctx__);
}
from = curr;
to = next;
return true;
}
复制代码
共享栈协程的切换流程与非共享栈协程的切换不同(需要拷贝有效栈内存),因此会有不同的处理。具体细节留待后面共享栈章节专门讨论。
共享栈协程是不能移动的,因为共享同一个栈的协程不能同时运行,所以他们必需归属于同一个 env,而且共享栈是 env 的资源,只能由 env 关联的线程操作与维护。
因此共享栈也是判断一个协程是否可移动的一个因素(source/cocpp/core/co_ctx.cpp):
bool co_ctx::can_move() const
{
return !(state() == co_state::running || state() == co_state::finished || test_flag(CO_CTX_FLAG_BIND) || test_flag(CO_CTX_FLAG_SHARED_STACK) || test_flag(CO_CTX_FLAG_SWITCHING));
}
复制代码
清除时机
此标识不会清除。
状态
ctx 共有三种状态,定义位于 include/cocpp/core/co_type.h
enum class co_state : unsigned char
{
suspended, // 暂停
running, // 运行
finished, // 结束
};
复制代码
其转换关系可以参照前面的章节。
suspended 暂停状态
所有还需要调度且当前没有被调度的协程都处于暂停状态(包括因等待资源阻塞的协程)。
设置或转换时机
当协程创建的时候,默认就是暂停状态(idle 协程除外,他创建的时候是 running 状态,因为和他关联的执行流就是当前线程的执行流,见 source/cocpp/core/co_env_factory.cpp):
co_env* co_env_factory::create_env(size_t stack_size)
{
auto idle_ctx = create_idle_ctx__();
auto shared_stack = stack_factory__->create_stack(stack_size);
auto ret = env_pool__.create_obj(shared_stack, idle_ctx, true);
assert(ret != nullptr);
idle_ctx->set_state(co_state::running);
return ret;
}
复制代码
但是我们查看创建 ctx 的实现,发现并没有设置初始状态是 suspended 的代码,这是怎么回事呢?
查看一下 ctx 中状态管理器的定义(include/cocpp/core/co_ctx.h):
co_state_manager<co_state, co_state::suspended, co_state::finished> state_manager__; // 状态管理
复制代码
此处是一个模板类特化,查看模板类实现(include/cocpp/utils/co_state_manager.h):
template <typename T, T InitState, T FinalState>
class co_state_manager final
{
private:
mutable co_spinlock mu_state__ { co_spinlock::lock_type::in_thread };
T state__ { InitState };
public:
void set_state(const T& state);
T state() const;
};
/////////////////////////////////// 模板实现 /////////////////////////////
template <typename T, T InitState, T FinalState>
void co_state_manager<T, InitState, FinalState>::set_state(const T& state)
{
std::lock_guard<co_spinlock> lock(mu_state__);
if (state__ != FinalState)
{
state__ = state;
}
}
template <typename T, T InitState, T FinalState>
T co_state_manager<T, InitState, FinalState>::state() const
{
std::lock_guard<co_spinlock> lock(mu_state__);
return state__;
}
复制代码
可以看到,模板类的第二个模板参数其实就是状态初值,那么最后一个模板参数是什么意思呢?最后一个模板参数其实是状态终值,查看 set_state 的实现,可以发现当状态到达终值状态时,就不能再变更为其他状态了。也就是说,当 ctx 的状态到达 finished 状态后,就不能转换为 suspended 或者 running 状态了(符合逻辑)。
第二种状态更新为 suspended 的场景是协程切出(source/cocpp/core/co_env.cpp):
void co_env::update_ctx_state__(co_ctx* curr, co_ctx* next)
{
// 如果当前运行的ctx已经完成,状态不变
if (curr->state() != co_state::finished)
{
curr->set_state(co_state::suspended);
}
next->set_state(co_state::running);
}
bool co_env::prepare_to_switch(co_ctx*& from, co_ctx*& to)
{
co_ctx* curr = current_ctx();
co_ctx* next = next_ctx__();
assert(curr != nullptr);
assert(next != nullptr);
set_flag(CO_ENV_FLAG_SCHEDULED);
update_ctx_state__(curr, next);
if (curr == next)
{
return false;
}
// ......
}
复制代码
在协程切换过程中会将当前协程转换为 suspended 状态(已经在 finished 状态的除外),并将下一个要执行的协程从 suspended 切换为 running 状态。
检测时机
suspended 状态目前不需要检测。
running 运行状态
运行状态表示 ctx 正在被 env 调度执行。
设置或转换时机
idle 协程被创建的时候是 running 状态,上文已经分析过。
协程被调度时会转换为 running 状态,上文分析过。
检测时机
running 状态仅在检测协程是否可以被移动时被检测(source/cocpp/core/co_ctx.cpp):
bool co_ctx::can_move() const
{
return !(state() == co_state::running || state() == co_state::finished || test_flag(CO_CTX_FLAG_BIND) || test_flag(CO_CTX_FLAG_SHARED_STACK));
}
复制代码
正在运行的协程不能被迁移到其他线程上运行。
finished 完成状态
完成状态表示此 ctx 不再需要被调度,他已经完成了所有任务。
设置或转换时机
协程函数执行结束后,就不需要被调度了,设置状态为 finished(source/cocpp/core/co_ctx.cpp):
void co_ctx::real_entry(co_ctx* ctx)
{
ctx->entry()(ctx->ret_ref());
// CO_DEBUG("ctx %s %p finished", ctx->config().name.c_str(), ctx);
ctx->set_state(co_state::finished);
ctx->finished().pub();
assert(ctx->env() != nullptr);
ctx->env()->schedule_switch(); // 此处的ctx对应的env不可能为空,如果为空,这个ctx就不可能被调度
}
复制代码
测试时机
协程执行完毕后,自然就不需要被调度,因此是否 finished 是是否可以被调度的一个决定因素(source/cocpp/core/co_ctx.cpp):
bool co_ctx::can_schedule() const
{
return state() != co_state::finished && !test_flag(CO_CTX_FLAG_WAITING);
}
复制代码
finished 状态的协程没有迁移的必要了,因此不需要迁移(迁移是为了解决资源不均衡或者消除系统调用影响)(source/cocpp/core/co_ctx.cpp)。
bool co_ctx::can_move() const
{
return !(state() == co_state::running || state() == co_state::finished || test_flag(CO_CTX_FLAG_BIND) || test_flag(CO_CTX_FLAG_SHARED_STACK));
}
复制代码
等待协程运行结束,其实就是等待协程状态变更为 finished(source/cocpp/core/co_env.cpp):
co_return_value co_env::wait_ctx(co_ctx* ctx)
{
auto old_priority = current_ctx()->priority();
current_ctx()->set_priority(ctx->priority());
CoDefer([this, old_priority] {
current_ctx()->set_priority(old_priority);
});
while (ctx->state() != co_state::finished)
{
schedule_switch();
}
wait_ctx_finished().pub(ctx);
return ctx->ret_ref();
}
复制代码
移除一个协程上下文的前提是这个协程已经执行结束了(source/cocpp/core/co_env.cpp):
void co_env::remove_detached_ctx__()
{
auto curr = current_ctx();
auto all_ctx = all_scheduleable_ctx__();
for (auto& ctx : all_ctx)
{
// 注意:此处不能删除当前的ctx,如果删除了,switch_to的当前上下文就没地方保存了
if (ctx->state() == co_state::finished && ctx->can_destroy() && ctx != curr)
{
remove_ctx(ctx);
}
}
}
复制代码
当协程状态已经是 finished,就没必要去更新状态了(source/cocpp/core/co_env.cpp)。
void co_env::update_ctx_state__(co_ctx* curr, co_ctx* next)
{
// 如果当前运行的ctx已经完成,状态不变
if (curr->state() != co_state::finished)
{
curr->set_state(co_state::suspended);
}
next->set_state(co_state::running);
}
复制代码
总结
本文对 ctx 的标识与状态做了详尽的分析,包括各种标识的含义、设置与清除场景、用途,以及各种状态的关系,检测时机等内容。信心量较大,阅读时可参照源码,加深理解。
评论