写点什么

一个 cpp 协程库的前世今生(四)协程上下文 ctx

作者:SkyFire
  • 2021 年 12 月 31 日
  • 本文字数:5955 字

    阅读完需:约 20 分钟

一个cpp协程库的前世今生(四)协程上下文ctx

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


GitHub - skyfireitdiy/cocpp at cocpp-0.1.0


之前的章节提到,ctx、env 和 manger 是 cocpp 的核心组件,今天就来着重介绍一下 ctx,ctx 的声明在 include/core/co_ctx.h 中。


这里将 ctx 中的成员罗列一下(删除了条件编译的宏代码):


    co_flag_manager<CO_CTX_FLAG_MAX>                                    flag_manager__;                                        // flag 管理    co_state_manager<co_state, co_state::suspended, co_state::finished> state_manager__;                                       // 状态管理    co_ctx_wait_data                                                    wait_data__ {};                                        // 等待数据    co_stack*                                                           stack__ { nullptr };                                   // 当前栈空间    co_ctx_config                                                       config__ {};                                           // 协程配置    co_any                                                              ret__;                                                 // 协程返回值,会被传递给 config 中的 entry    co_env*                                                             env__ { nullptr };                                     // 协程当前对应的运行环境    mutable co_spinlock                                                 env_lock__ { co_spinlock::lock_type::in_thread };      // 运行环境锁    int                                                                 priority__ { CO_IDLE_CTX_PRIORITY };                   // 优先级    mutable co_spinlock                                                 priority_lock__ { co_spinlock::lock_type::in_thread }; // 优先级锁    std::unordered_map<std::string, std::shared_ptr<co_local_base>>     locals__;                                              // 协程局部存储    std::function<void(co_any&)>                                        entry__;                                               // 协程入口函数    co_byte* regs__[32] {};                                                                                                    // 协程寄存器
复制代码


下面对这些成员做一下说明

标识管理器 flag_manager__

主要用来管理 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_SWITCHING    = 5; // 正在切换constexpr int CO_CTX_FLAG_MAX          = 8; // 最大标志位
复制代码


标识位描述了 ctx 当前的一些特征或者状态(主要是一些附加状态,主要状态由状态管理器控制)


co_flag_manager 是一个模板类实现,位于 include/utils/co_flag_manager.h 中:


template <size_t MAX_FLAG_COUNT>class co_flag_manager final{private:    std::bitset<MAX_FLAG_COUNT> flags__;    mutable co_spinlock         mu__ { co_spinlock::lock_type::in_thread };
public: void set_flag(size_t flag) { std::lock_guard<co_spinlock> lock(mu__); flags__.set(flag); } void reset_flag(size_t flag) { std::lock_guard<co_spinlock> lock(mu__); flags__.reset(flag); } bool test_flag(size_t flag) const { std::lock_guard<co_spinlock> lock(mu__); return flags__.test(flag); }};
复制代码


其内部由 std::bistset 管理,只是在访问时加了锁,关于自旋锁 co_spinlock 的实现,后面章节会介绍。

状态管理器 state_manager__

状态管理器主要管理 ctx 的当前调度状态,各种状态值定义在 include/core/type.h 中:


enum class co_state : unsigned char{    suspended, // 暂停    running,   // 运行    finished,  // 结束};
复制代码


ctx 只有暂停、运行和结束三个状态,三个状态的转换关系如下:



  • 新创建的 ctx 是 suspended 状态,会在调度器的 ctx 队列中等待被调度。

  • 当调度器选中 1 个在 suspended 状态的 ctx 之后,这个 ctx 会变成 running 状态,同时 ctx 中保存的上下文信息也会被恢复到 env 中执行。

  • 当处于 running 状态的 ctx 由于加锁、主动切换、或者由于超时未调度被 manager 强制调度,都会导致状态更新为 suspended,同时 env 会将当前运行环境的上下文信息保存到 ctx 中。

  • 当协程函数执行完毕后,ctx 的状态会变更为 finished。

等待数据 wait_data__

这个成员变量主要是负责管理当前协程正在等待的资源,这一部分先留到后面协程等待的相关章节再深入了解。

协程栈 stack__

协程栈是当前线程的运行时栈内存空间,结构定义如下(位于 include/core/co_stack.h)。


class co_stack final : private co_noncopyable{    co_byte* stack__;                          // 堆栈指针    size_t   size__;                           // 堆栈大小    co_stack(co_byte* ptr, size_t stack_size); // 构造函数public:    size_t   stack_size() const; // 堆栈大小    co_byte* stack() const;      // 堆栈指针    co_byte* stack_top() const;  // 堆栈顶部指针
friend class co_object_pool<co_stack>;};
复制代码


类的内部保存了栈内存的起始地址和大小。


注意此处的构造函数是私有的,这个类的实例化只能通过对象池进行,对象池的介绍留在后续章节中。

协程配置 config__

协程配置是在创建 ctx 的时候传入的属性,相关结构体定义在 include/core/co_ctx_config.h


class co_env;
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 }; // 共享栈};
// 以下宏代码生成 with_xxx 的配置选项#define CO_GEN_CTX_CONFIG_OPTION(type, name) \ inline std::function<void(co_ctx_config&)> with_##name(type p##name) \ { \ return [p##name](co_ctx_config& config) { config.name = p##name; }; \ }
#define CO_GEN_CTX_CONFIG_OPTION_HELPER(type, name) \ CO_GEN_CTX_CONFIG_OPTION(type, name)
CO_GEN_CTX_CONFIG_OPTION_HELPER(size_t, stack_size)CO_GEN_CTX_CONFIG_OPTION_HELPER(std::string, name)CO_GEN_CTX_CONFIG_OPTION_HELPER(size_t, priority)CO_GEN_CTX_CONFIG_OPTION_HELPER(co_env*, bind_env)CO_GEN_CTX_CONFIG_OPTION_HELPER(bool, shared_stack)
复制代码


这里的配置主要有:


  • 栈大小,默认为 CO_DEFAULT_STACK_SIZE = 1024 * 1024 * 8(8MB)

  • 协程名,可以为协程配置一个人类可读的名字。默认__unknown__

  • 优先级,0~99。0 为最高优先级,99 为最低,默认为 99

  • 执行环境绑定,如果配置了此选项,则当前协程只会在指定的环境中运行,不会被迁移到其他的环境

  • 共享栈,是否与当前环境下其他协程共享栈空间,该选项在后面共享栈相关章节做介绍。


注意下面有一个宏,自动生成设置相关属性的函数。

返回值 ret__

该字段存储协程函数的返回值,当协程完成之后这个值会被设置。这个在后面的返回值实现相关章节再做介绍。

当前执行环境 env__及其保护锁 env_lock__

需要注意的一点是,这里说的是当前的执行环境,也就是说在某些时刻这个值是会发生变化的,协程可以挂在不同的环境下执行(共享栈的协程和指定绑定了运行环境的协程不能迁移)。

优先级 priority__ 及其保护锁 priority_lock__

协程的真正优先级。可能有人注意到,上面的 config__不是已经指定了优先级吗,为什么此处还有一个优先级的字段?


上面的配置只是在协程创建的时候为它设置一个初始的优先级。而在协程运行过程中,有一些情况会让优先级发生改变。比如一个高优先级的协程等待一个低优先级协程持有的锁,因为高优先级协程的存在,导致低优先级,永远得不到调度,进而导致低优先级的协程一直无法释放锁,而高优先级的协程也因为一直拿不到锁而空等待,于是就会造成死锁。解决此问题的办法就是临时调整协程的优先级,使低优先级的协程可以得到调度。


因此还需要一个字段来存储协程当前真正的优先级。

协程局部存储 locals__

这个到后面协程局部存储相关章节再进行介绍。

协程入口函数 entry__

协程的真正入口函数,注意这里的参数是 co_any&,而我们上层应用需要的协程入口函数需要可以接受任何类型的参数,因此这里的入口函数应该是上层任意函数的一个封装。

寄存器信息 regs__

在 x86_64 的 linux 下,其真实的结构应该是 sigcontext_64,定义在 source/core/co_vos_gcc_x86_64.cpp 中:


struct sigcontext_64{    unsigned long long r8;    // 0*8    unsigned long long r9;    // 1*8    unsigned long long r10;   // 2*8    unsigned long long r11;   // 3*8    unsigned long long r12;   // 4*8    unsigned long long r13;   // 5*8    unsigned long long r14;   // 6*8    unsigned long long r15;   // 7*8    unsigned long long di;    // 8*8    unsigned long long si;    // 9*8    unsigned long long bp;    // 10*8    unsigned long long bx;    // 11*8    unsigned long long dx;    // 12*8    unsigned long long ax;    // 13*8    unsigned long long cx;    // 14*8    unsigned long long sp;    // 15*8    unsigned long long ip;    // 16*8    unsigned long long flags; // 17*8
// 后面这部分获取不到,暂时不用
// unsigned short cs; //18*8 // unsigned short gs; //18*8+2 // unsigned short fs; //18*8+4 // unsigned short __pad0; //18*8+6 // unsigned long long err; //19*8 // unsigned long long trapno; //20*8 // unsigned long long oldmask; //21*8 // unsigned long long cr2; //22*8 // unsigned long long fpstate; //23*8 // unsigned long long reserved1[8]; //24*8};
复制代码


这个结构比我们在协程切换原理那一个章节介绍的 8 个寄存器要多的多,这是因为需要支持外部调度。况且存多了比存少了要好。这个到后面外部切换相关的章节再进行介绍。

成员函数以及静态函数

以下是 ctx 中所有的成员函数:


    void                         set_priority(int priority);                       // 设置优先级    size_t                       priority() const;                                 // 获取优先级    bool                         can_schedule() const;                             // 判断是否可以调度    co_stack*                    stack() const;                                    // 获取当前栈空间    co_byte**                    regs();                                           // 获取寄存器    const co_ctx_config&         config() const;                                   // 获取配置    co_any&                      ret_ref();                                        // 获取返回值    void                         set_env(co_env* env);                             // 设置运行环境    co_env*                      env() const;                                      // 获取运行环境    bool                         can_destroy() const;                              // 判断是否可以销毁    void                         lock_destroy();                                   // 锁定销毁    void                         unlock_destroy();                                 // 解锁销毁    void                         set_stack(co_stack* stack);                       // 设置栈空间    bool                         can_move() const;                                 // 判断是否可以移动    std::string                  name() const;                                     // 获取名称    co_id                        id() const;                                       // 获取id    void                         enter_wait_resource_state(int rc_type, void* rc); // 进入等待资源状态    void                         leave_wait_resource_state();                      // 离开等待资源状态    std::function<void(co_any&)> entry() const;                                    // 获取入口函数    static void                  real_entry(co_ctx* ctx);                          // 协程入口函数(内部实际的入口)    void                         set_state(const co_state& state);                 // 设置ctx状态    void                         set_flag(size_t flag);                            // 设置标识    void                         reset_flag(size_t flag);                          // 重置标识
template <typename T> T& local_storage(const std::string& name); // 获取局部存储
CoConstMemberMethodProxy(&flag_manager__, test_flag); // 测试标志 CoConstMemberMethodProxy(&state_manager__, state); // 获取状态
复制代码


从函数的命名和后面的注释大致就可以了解函数的作用。函数的具体实现到后面介绍协程执行的生命周期相关章节再进行介绍。

总结

本文对写成上下文中所有的字段进行了逐一介绍,对协程的状态转换进行了简要说明,对成员中可能会导致理解偏差的地方也做出了详细的解释。希望对后续章节的理解有所帮助。c

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

SkyFire

关注

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

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(四)协程上下文ctx