写点什么

一个 cpp 协程库的前世今生(七)ctx 的状态与标志位

作者:SkyFire
  • 2022 年 1 月 01 日
  • 本文字数:8836 字

    阅读完需:约 29 分钟

一个cpp协程库的前世今生(七)ctx的状态与标志位

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


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 ctxconstexpr 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;}
复制代码

检测时机

  • 初始化 ctx 的时候(source/cocpp/core/co_vos_gcc_x86_64.cpp):


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 协程被创建的时候

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 的标识与状态做了详尽的分析,包括各种标识的含义、设置与清除场景、用途,以及各种状态的关系,检测时机等内容。信心量较大,阅读时可参照源码,加深理解。

发布于: 4 小时前
用户头像

SkyFire

关注

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

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(七)ctx的状态与标志位