为了防止大家找不到代码,还是先贴一下项目链接:
GitHub - skyfireitdiy/cocpp at cocpp-0.1.0
本文将介绍系列文章中的最后一篇,共享栈的实现。
定义
共享栈协程的定义为,多个协程共享一段运行时的栈空间,以此达到节省内存空间的目的。
优缺点及应用场景
实现原理
共享栈协程的实现原理并不复杂,只是在切换时需要一些额外的处理逻辑。
如上图,右侧为实际的运行时栈,通常比较大。而每个共享栈协程的协程对象中会有一个小空间来存储自身的有效栈空间。当共享站的协程发生切换时,当前正在运行的协程需要将运行时栈中有效的数据保存到当前协程对象中,另外需要将下一个将要运行的协程对应的协程对象中保存的数据恢复到运行时栈中,然后切换指令的位置。
正在运行的协程想要保存运行是栈中有效的数据是很困难的,因此我们需要第 3 个协程来辅助完成这个操作。如下图所示:
图中蓝色的连接线表示协程的切换流程,红色的线表示协程对应的协程栈,绿色的线表示运行时栈的变化顺序。
初始状态时,运行时栈中的内容是当前共享栈协程栈。对应关系为线 1 所示
当前共享协程需要发生切换时,临时切换到共享栈切换协程。这时候运行时栈切换到共享栈切换协程栈。如线 2 所示。
此时当前协程已经被停止运行了,共享栈的内容不再发生改变,所以共享栈切换协程可以放心地保存当前共享栈的内容保存到当前协程的协程对象中。再把下一个共享协程的栈内存恢复到共享栈中。然后切换到下一个共享栈协程。此时的关系如线 3 所示。
实现细节
共享栈何时创建?
每一个执行环境会有一个与之关联的共享栈空间,在执行环境创建的时候,对应的共享栈空间就会创建出来。具体实现代码位于 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;
}
复制代码
共享栈切换协程在哪里?
有上面的讨论可知共享栈的切换需要第 3 个协程辅助完成。那么第 3 个协程在 cocpp 中是如何体现出来的呢?
事实上,cocpp 将 idle 协程与共享栈切换协程功能合一了。idle 协程中实现了共享栈切换协程的功能。
我们先看一下协程切换的实现(source/cocpp/core/co_env.cpp):
bool co_env::prepare_to_switch(co_ctx*& from, co_ctx*& to)
{
co_ctx* curr = current_ctx();
co_ctx* next = next_ctx__();
// ...
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;
你们。.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;
}
复制代码
可以看到,如果当前线程或者是下一个要切换的协程是共享栈协程,那么此时会将 shared_stack_switch_info__中的一些数据成员赋值。然后将 next 设值为 idle 协程。此时就会切换到 idle 协程。
当切换到 idle 协程之后,会再次进入 schedule_switch 函数中。
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)
{
decreate_interrupt_lock_count_with_lock();
schedule_switch();
increate_interrupt_lock_count_with_lock();
// 切换回来检测是否需要执行共享栈切换
if (shared_stack_switch_info__.need_switch)
{
continue;
}
// ...
}
// ...
}
复制代码
接下来看一下 schedule_switch 函数。
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();
}
复制代码
此函数会根据 shared_stack_switch_info__.need_switch 的值判断是否需要共享栈切换。如果需要共享栈切换,就会调用 switch_shared_stack_ctx__。接下来我们来看一下这个函数的实现。
void co_env::switch_shared_stack_ctx__()
{
shared_stack_switch_info__.need_switch = false;
if (shared_stack_switch_info__.from->test_flag(CO_CTX_FLAG_SHARED_STACK))
{
save_shared_stack__(shared_stack_switch_info__.from);
}
if (shared_stack_switch_info__.to->test_flag(CO_CTX_FLAG_SHARED_STACK))
{
restore_shared_stack__(shared_stack_switch_info__.to);
}
// 切换到to
unlock_schedule();
decreate_interrupt_lock_count_with_lock();
switch_to(idle_ctx__->regs(), shared_stack_switch_info__.to->regs());
increate_interrupt_lock_count_with_lock();
lock_schedule();
switched_to().pub(idle_ctx__);
}
复制代码
在子函数中会分别判断上一个协程和下一个协程是否是共享栈,然后决定是否需要保存共享栈空间的内容和恢复共享栈空间的内容。
保存和恢复共享栈空间的逻辑比较简单,此处就不再叙述了。
至此整个共享栈协程的切换就讨论结束了。
需要注意的一点是,cocpp 中共享栈空间是与执行环境绑定的,也就意味着如果一个共享栈协程被分配到执行环境 X 上,那么它的整个生命周期将与 X 绑定,不会被迁移到其他的执行环境上。
总结
本文主要讲述了共享栈空间的协程切换细节,总体叙述比较粗,只是将原理大致串了一下。如果需要了解更多细节,建议参照源码。
评论