写点什么

一个 cpp 协程库的前世今生(五)协程执行环境 env

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

    阅读完需:约 13 分钟

一个cpp协程库的前世今生(五)协程执行环境env

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


GitHub - skyfireitdiy/cocpp at cocpp-0.1.0


env 是协程的实际运行环境,也是三个基础组件中最复杂的数据结构。其成员如下,定义于 include/core/co_env.h


    co_flag_manager<CO_ENV_FLAG_MAX>                                                flag_manager__;                                          // 标志管理器    co_state_manager<co_env_state, co_env_state::created, co_env_state::destorying> state_manager__;                                         // 状态管理器    std::future<void>                                                               worker__;                                                // 工作线程    co_sleep_controller                                                             sleep_controller__;                                      // 睡眠控制器    co_stack*                                                                       shared_stack__ { nullptr };                              // 共享栈    co_ctx* const                                                                   idle_ctx__ { nullptr };                                  // 空闲协程    co_tid                                                                          schedule_thread_tid__ {};                                // 调度线程tid    std::vector<std::list<co_ctx*>>                                                 all_normal_ctx__ { CO_MAX_PRIORITY };                    // 所有普通协程    mutable co_spinlock                                                             mu_normal_ctx__ { co_spinlock::lock_type::in_thread };   // 普通协程锁    std::unordered_set<co_ctx*>                                                     blocked_ctx__;                                           // 被阻塞的协程    mutable co_spinlock                                                             mu_blocked_ctx__ { co_spinlock::lock_type::in_thread };  // 阻塞协程锁    co_ctx*                                                                         curr_ctx__ { nullptr };                                  // 当前协程    mutable co_spinlock                                                             mu_curr_ctx__ { co_spinlock::lock_type::in_thread };     // 当前协程锁    int                                                                             min_priority__ = 0;                                      // 最小优先级    mutable co_spinlock                                                             mu_min_priority__ { co_spinlock::lock_type::in_thread }; // 最小优先级锁    co_spinlock                                                                     schedule_lock__ { co_spinlock::lock_type::in_thread };   // 调度锁    struct    {                                  //        co_ctx* from { nullptr };      // 从哪个ctx切换        co_ctx* to { nullptr };        // 切换到哪个ctx        bool    need_switch { false }; // 是否需要切换    } shared_stack_switch_info__;      // 共享栈上的ctx切换信息
复制代码


接下来对其成员做一些剖析。

标识管理器 flag_manager__

记录了执行环境一些属性。其取值在 include/core/co_define.h 中定义:


constexpr int CO_ENV_FLAG_NO_SCHE_THREAD    = 0; // 没有调度线程constexpr int CO_ENV_FLAG_COVERTED          = 1; // 从正常线程转换来的调度线程constexpr int CO_ENV_FLAG_SCHEDULED         = 2; // 被调度过constexpr int CO_ENV_FLAG_DONT_AUTO_DESTORY = 3; // 禁止被自动清理线程选中constexpr int CO_ENV_FLAG_MAX               = 8; // 最大FLAG值
复制代码


第 1 个标识 CO_ENV_FLAG_NO_SCHE_THREAD 的含义是指当前 env 没有与之关联的调度线程。那么可能有人会有疑问,前面不是说每个执行环境都与一个内核线程相关联吗,为什么这里又说执行环境没有关联的调度线程?


注意这里的区别是没有调度线程相关联,并不是没有线程相关联。调度线程是指用来调度协程执行的线程。但是有一些线程是不会参与协程调度的,比如我们自己创建的一些线程。那为什么这些线程也会有与之关联的 env 呢?因为会有这一种情况,我们在自己的线程中创建了协程,然后这个协程被分配到相应的调度线程去执行。但是我们又要在当前线程里面等待他执行结束,这个等待过程是需要 env 结构支撑的,因此会为当前的线程创建一个与之关联的 env,这个 env 不具有调度协程的能力,但是却可以用来做一些协程的状态检查,或者是等待操作。对于没有调度能力的 env,我们在创建协程的时候不会将新协程分配给它。


对于第 2 个标识 CO_ENV_FLAG_COVERTED ,他与第 1 个标志有一定的关联。当我们希望自己创建的线程也具有调度能力的时候,可以调用一个转换函数来将当前线程转换为可调度协程的线程(见 env 的成员函数 schedule_in_this_thread)。


第 3 个标识 CO_ENV_FLAG_SCHEDULED,其定义为在一个监控周期内该协程执行环境发生过调度。当后台监控线程发现在一个监控周期内当前线程都没有发生过调度,就会认为当前线程陷入了系统调用。会将当前调度线程上所有可迁移的协程迁移到其他的执行环境上运行。


第 4 个标识 CO_ENV_FLAG_DONT_AUTO_DESTORY 的含义是不自动销毁此执行环境。当后台监控线程发现协程的执行环境超过我们设置的上限阈值时,会选取一部分协程环境进行销毁。如果我们设置了这个标志,就可以避免被销毁,这个标志用于从用户线程转为调度线程的执行环境。

状态管理器 state_manager__

执行环境的状态定义在 include/core/co_type.h,如下:


enum class co_env_state : unsigned char{    idle,       // 空闲状态,此时可以将ctx加入到env中,或者释放此env    busy,       // 繁忙状态,可以将ctx加入到env    blocked,    // 阻塞状态,需要创建更多的env分担任务    destorying, // 正在销毁状态,不再调度ctx    created,    // 创建完成状态,此env不能用于调度ctx,通常是普通线程适配产生的env};
复制代码


idle 状态表示当前没有可调度的协程,如果有新创建的协程可以优先分配给当前执行环境。


busy 状态表示当前至少有一个协程,正在被调度,且可以接受新的协程。


blocked 状态表示当前执行环境可能陷入了死循环或者是系统调用(在一个监控周期内没有被调度),此时需要从外部强制调度,或者将当前环境的其他可移动协程迁移到其他的执行环境上。


destorying 当前执行环境正在被销毁。


created 当前执行环境被创建,还不具备调度能力,通常与上面的标识 CO_ENV_FLAG_NO_SCHE_THREAD 一同存在。

工作线程 worker__

与当前 env 关联的内核线程。

睡眠控制器 sleep_controller__

睡眠控制器主要使空闲的执行环境休眠,减少 CPU 占用。此部分留到后续相关章节进行介绍。

共享栈 shared_stack__

一个特殊的栈空间,用于设置了共享栈标识的协程共享。共享栈在后面的章节单独介绍,此处不做展开。

空闲协程 idle_ctx__

空闲协程是一个特殊的协程,类似于 linux 操作系统的 1 号进程,当没有协程需要调度的时候,就会执行 idle 协程,idle 协程的执行流程是一个死循环调度(在销毁环境的时候,此循环会退出)。特别的,idle 协程没有开辟自己的协程栈空间,直接使用当前线程的栈空间。

当前线程 id:schedule_thread_tid__

schedule_thread_tid__存储了当前线程的线程 ID。主要用于调试方便。

所有可调度协程 all_normal_ctx__ 及其保护锁 mu_all_normal_ctx__

这是一个向量,共 100 个元素,每个元素对应一个优先级,从 0~99。每个元素是一个列表,存储了对应优先级下的所有协程。调度的时候,调度器会优先调度优先级高的协程(对应的是下标较小)。而对于同等优先级的协程采用轮巡的方式调度。

阻塞协程列表 blocked_ctx__及其保护锁 mu_blocked_ctx__

当一个协程在运行时由于等待资源阻塞,就会将其从可调度写成列表中移除,添加到阻塞协程列表中。等该协程被唤醒时,再从阻塞协程列表转移到可调度协程列表。

当前运行协程 curr_ctx__ 及其保护锁 mu_curr_ctx__

顾名思义,就是当前正在运行的协程。但是有一点需要注意,当可调度列表为空时,这个值为 nullptr,此时运行的是 idle 协程。

当前最小优先级 min_priority__ 及其保护锁 mu_min_priority__

由于可调度协程链表有 100 项,所有新创建的协程默认的优先级都是 99,因此在查找下一个可调度的协程时,如果从 0 开始遍历列表效率就太低了。所以此处记录了最小的优先级,也就是当前最高优先级。直接从此处开始遍历在大多情况下只需要一次就可以找到可运行的协程。


需要注意的是,当协程加入或者协程的优先级改变的时候需要同步更新 min_priority__。

调度锁 schedule_lock__

调度锁的目的是为了保护调度过程。监控线程可能在任意时刻迁移调度线程下的协程,为了保证这一操作的原子性,需要在这一段时间禁止调度,因此增加了调度锁。

共享栈切换信息 shared_stack_switch_info__

这个结构是为了辅助共享栈切换使用的,具体细节在后面的共享站章节介绍。

总结

本文对线程执行环境结构中的成员作了分析解释,希望读者能更好地理解后面的章节。

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

SkyFire

关注

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

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(五)协程执行环境env