协程模块
1️⃣ 什么是协程?
协程是一种比线程更轻量级的用户态并发单位,可以在一个线程内部并发执行多个任务,并且支持在任务之间进行主动挂起与恢复,从而实现看起来像并行实际上是串行的异步逻辑。

2️⃣ ucontext
在 sylar 框架中,协程 Fiber 是基于 Linux 提供的 <ucontext.h> 实现的,该头文件中的 API 提供了保存、切换、恢复程序执行上下文的能力,允许用户在用户态实现轻量级的协程调度机制。
概述
ucontext 是 POSIX 标准中定义的一组函数,用于实现用户级上下文切换。它允许程序保存和恢复执行上下文(如寄存器、程序计数器、栈等),常用于实现协程、轻量级线程或任务调度等功能。以下是对 ucontext 的详细介绍,并结合代码进行说明。
ucontext 是实现协程的一种方式。因为协程本质上就是一种用户级的线程, 既然是一种特殊的线程, 其上下文切换的实现方式应该和线程的上下文切换类似, 也需要保存必要的寄存器、程序计数器、栈等信息。
核心功能
ucontext 提供了一种在用户空间管理执行上下文的方式,避免了内核级线程切换的开销。它的核心功能包括:
- 保存当前执行上下文。
- 恢复之前保存的上下文。
- 创建新的执行上下文。
- 在上下文之间切换。
核心数据结构 ucontext_t
ucontext_t 是 ucontext 的核心数据结构,定义如下:
1 | typedef struct ucontext { |
核心函数
getcontext : 保存当前上下文
1 | int getcontext(ucontext_t *ucp); |
- 功能: 将当前执行上下文保存到
ucp指向的ucontext_t结构体中。 - 返回值: 成功返回 0,失败返回 -1。
- 用途: 通常用于保存当前状态,以便后续恢复。
setcontext : 恢复上下文
1 | int setcontext(const ucontext_t *ucp); |
- 功能: 从
ucp指向的ucontext_t结构体恢复上下文。 - 返回值: 如果成功,不会返回;如果失败,返回 -1。
- 用途: 用于跳转到之前保存的上下文。
makecontext : 创建新上下文
1 | void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); |
- 功能: 为
ucp设置一个新的上下文,指定入口函数func和参数。 - 参数:
ucp: 需要初始化的上下文。func: 新上下文的入口函数。argc: 传递给func的参数个数。...: 具体的参数值。
- 用途: 用于跳转到之前保存的上下文。
swapcontext : 切换上下文
1 | int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); |
- 功能: 保存当前上下文到
oucp,并切换到ucp指定的上下文。 - 返回值: 如果成功,不会返回;如果失败,返回 -1。
- 用途: 用于在两个上下文之间切换。
3️⃣ 整体框架

模块解析
私有成员
m_id:协程 idm_stacksize:协程栈的大小m_ctx:协程上下文m_stack:协程栈地址m_cb:协程入口函数m_state:协程的状态,主要有Ready,RUNNING,TERM组成m_runInScheduler:本协程是否参与调度器Scheduler调度
全局变量
s_fiber_id:- 类型:
static std::atomic<uint64_t> - 作用:全局静态变量,帮助生成协程 id
- 类型:
s_fiber_count- 类型:
static std::atomic<uint64_t> - 作用:全局静态变量,帮助统计协程的数量
- 类型:
t_fiber- 类型:
static thread_local Fiber* - 作用:静态线程局部变量,表示当前线程正在运行的协程
- 类型:
t_thread_fiber- 类型:
static thread_local Fiber::ptr - 作用:静态线程局部变量,表示当前线程的主协程,切换到这个协程,就相当于切换到了主线程中运行。
- 类型:
重要成员函数
SetThis1
2
3
4void Fiber::SetThis(Fiber *f)
{
t_fiber = f;
}- 作用:表示当前的协程为线程正在执行的协程,即设置线程局部变量
t_fiber
- 作用:表示当前的协程为线程正在执行的协程,即设置线程局部变量
GetThis1
2
3
4
5
6
7
8
9
10
11
12Fiber::ptr Fiber::GetThis()
{
if (t_fiber)
{
return t_fiber->shared_from_this();
}
Fiber::ptr main_fiber(new Fiber);
SYLAR_ASSERT(main_fiber.get() == t_fiber);
t_thread_fiber = main_fiber;
return t_fiber->shared_from_this();
}- 作用:
- 返回当前线程正在执行的协程,即返回线程局部变量
t_fiber - 如果当前线程还未创建协程,则创建线程的第一个协程,且该协程为当前线程的主协程,其他协程都通过这个协程来调度。
- 也就是说,其他协程结束时,都要切回主协程,由主协程重新选择新的协程进行
resume - 通过静态线程局部变量
t_thread_fiber保存当前的线程的主协程。
- 返回当前线程正在执行的协程,即返回线程局部变量
- 作用:
构造函数
Fiber()1
2
3
4
5
6
7
8
9
10
11
12
13Fiber::Fiber()
{
SetThis(this);
m_state = RUNNING;
if (getcontext(&m_ctx))
{
SYLAR_ASSERT2(false,"getcontent");
}
++s_fiber_count;
m_id = s_fiber_id++;
SYLAR_LOG_INFO(g_logger) << "Fiber::Fiber() main id = " << m_id;
}- 作用
- 无参构造函数主要创建线程的第一个协程,该协程为线程的主协程。
- 线程主协程在每个线程中由
GetThis()懒初始化并保存在t_thread_fiber中,外部应通过GetThis()获取,不应手动构造。
- 作用
Fiber(std::function<void()> cb, size_t stacksize, bool run_in_scheduler)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool run_in_scheduler)
: m_id(s_fiber_id++)
, m_cb(cb)
, m_runInScheduler(run_in_scheduler)
{
++s_fiber_count;
m_stacksize = stacksize ? stacksize : g_fiber_stack_size->getValue();
m_stack = StackAllocator::Alloc(m_stacksize);
if (getcontext(&m_ctx))
{
SYLAR_ASSERT2(false, "getcontext");
}
m_ctx.uc_link = nullptr;
m_ctx.uc_stack.ss_sp = m_stack;
m_ctx.uc_stack.ss_size = m_stacksize;
makecontext(&m_ctx, Fiber::MainFunc, 0);
SYLAR_LOG_INFO(g_logger) << "Fiber::Fiber() id=" << m_id;
}- 作用
- 有参构造函数主要用于创建子协程,该协程为线程主协程或者调度器主协程的子协程。
- 分配规定大小的堆空间,将开辟的堆空间用作协程栈的起始地址
- 通过
makecontext把协程入口设置为Fiber::MainFunc:切入该协程时从MainFunc开始执行,MainFunc内部会调用用户传入的回调m_cb,结束后将状态置为TERM并yield回主协程/调度器。
- 作用
析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23Fiber::~Fiber()
{
SYLAR_LOG_INFO(g_logger) << "Fiber::~Fiber() id = " << m_id;
--s_fiber_count;
if (m_stack) // 这说明有栈,这是个子协程
{
SYLAR_ASSERT(m_state == TERM);
StackAllocator::DeAlloc(m_stack, m_stacksize);
SYLAR_LOG_DEBUG(g_logger) << "dealloc stack, id = " << m_id;
}
else // 这说明没栈,这是个主协程
{
SYLAR_ASSERT(!m_cb);
SYLAR_ASSERT(m_state == RUNNING); // 主协程一定是执行状态
Fiber* cur = t_fiber; // 当前协程就是自己
if (cur == this)
{
SetThis(nullptr);
}
}
}- 作用
- 如果是子协程,说明
Fiber有单独的协议栈,需要释放掉申请的堆空间。 - 如果是主协程,说明
Fiber共享线程本身的栈空间,因此不需要释放。 - 清理当前线程所保存的线程局部变量 “当前指向的协程”,将其设置为
nullptr。
- 如果是子协程,说明
- 作用
reset1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void Fiber::reset(std::function<void()> cb)
{
SYLAR_ASSERT(m_stack);
SYLAR_ASSERT(m_state == TERM);
m_cb = cb;
if (getcontext(&m_ctx))
{
SYLAR_ASSERT2(false,"getcontext");
}
m_ctx.uc_link = nullptr;
m_ctx.uc_stack.ss_size = m_stacksize;
m_ctx.uc_stack.ss_sp = m_stack;
makecontext(&m_ctx,Fiber::MainFunc,0);
m_state = READY;
}- 作用
- 重置协程状态和入口函数,复用栈空间,不重新创建栈。
- 作用
resume1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void Fiber::resume()
{
SYLAR_ASSERT(m_state != TERM && m_state != RUNNING);
SetThis(this);
m_state = RUNNING;
// 如果协程参与调度器调度,那么应该和调度器的主协程进行swap,而不是线程主协程
if (m_runInScheduler)
{
if (swapcontext(&(Scheduler::GetMainFiber()->m_ctx), &m_ctx))
{
SYLAR_ASSERT2(false, "swapcontext");
}
}
else
{
if (swapcontext(&(t_thread_fiber->m_ctx), &m_ctx))
{
SYLAR_ASSERT2(false, "swapcontext");
}
}
}- 作用
- 设置线程正在执行的协程为当前协程以及协程状态为
RUNNING - 如果协程参与调度器调度,那么应该和调度器的主协程进行
swap,而不是线程主协程。
- 设置线程正在执行的协程为当前协程以及协程状态为
- 作用
yield1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26void Fiber::yield()
{
// 协程运行完之后会自动yield一次,用于回到主协程,此时状态已为结束状态
SYLAR_ASSERT(m_state == RUNNING || m_state == TERM);
if (m_state != TERM)
{
m_state = READY;
}
// 如果协程参与调度器调度,那么应该和调度器的主协程进行swap,而不是线程主协程
if (m_runInScheduler)
{
SetThis(Scheduler::GetMainFiber());
if (swapcontext(&m_ctx, &(Scheduler::GetMainFiber()->m_ctx)))
{
SYLAR_ASSERT2(false, "swapcontext");
}
}
else
{
SetThis(t_thread_fiber.get());
if (swapcontext(&m_ctx, &(t_thread_fiber->m_ctx)))
{
SYLAR_ASSERT2(false, "swapcontext");
}
}
}- 作用
- 当前协程重置自己的状态为
READY。 - 如果协程参与调度器调度,和调度器的主协程进行切换,让出自己的执行权。
- 如果协程不参与调度器调度,和线程的主协程进行切换,让出自己的执行权力。
- 当前协程重置自己的状态为
- 作用
MainFunc1
2
3
4
5
6
7
8
9
10
11
12
13
14void Fiber::MainFunc()
{
Fiber::ptr cur = GetThis();
SYLAR_ASSERT(cur);
cur->m_cb();
cur->m_cb = nullptr;
cur->m_state = TERM;
auto raw_ptr = cur.get(); // 手动让t_fiber的引用计数减1
cur.reset();
raw_ptr->yield();
SYLAR_ASSERT2(false, "never reach fiber_id=" + std::to_string(raw_ptr->getId()));
}- 作用
- 该函数为协程入口函数,用于每个子协程执行自己所绑定的函数,并重置子协程的状态为
TERM,最后让出执行权限。
- 该函数为协程入口函数,用于每个子协程执行自己所绑定的函数,并重置子协程的状态为
- 作用
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 GYu的妙妙屋!
