1️⃣ 什么是协程?

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

协程和线程的区别

2️⃣ ucontext

sylar 框架中,协程 Fiber 是基于 Linux 提供的 <ucontext.h> 实现的,该头文件中的 API 提供了保存、切换、恢复程序执行上下文的能力,允许用户在用户态实现轻量级的协程调度机制。

概述

ucontextPOSIX 标准中定义的一组函数,用于实现用户级上下文切换。它允许程序保存和恢复执行上下文(如寄存器、程序计数器、栈等),常用于实现协程、轻量级线程或任务调度等功能。以下是对 ucontext 的详细介绍,并结合代码进行说明。

ucontext 是实现协程的一种方式。因为协程本质上就是一种用户级的线程, 既然是一种特殊的线程, 其上下文切换的实现方式应该和线程的上下文切换类似, 也需要保存必要的寄存器程序计数器等信息。

核心功能

ucontext 提供了一种在用户空间管理执行上下文的方式,避免了内核级线程切换的开销。它的核心功能包括:

  • 保存当前执行上下文。
  • 恢复之前保存的上下文。
  • 创建新的执行上下文。
  • 在上下文之间切换。

核心数据结构 ucontext_t

ucontext_tucontext 的核心数据结构,定义如下:

1
2
3
4
5
6
7
typedef struct ucontext {
struct ucontext *uc_link; // 指向下一个上下文
sigset_t uc_sigmask; // 信号掩码
stack_t uc_stack; // 栈信息
mcontext_t uc_mcontext; // 机器上下文(寄存器等)
// 其他实现相关的字段
} ucontext_t;

核心函数

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:协程 id
  • m_stacksize:协程栈的大小
  • m_ctx:协程上下文
  • m_stack:协程栈地址
  • m_cb:协程入口函数
  • m_state:协程的状态,主要有 ReadyRUNNINGTERM 组成
  • 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
    • 作用:静态线程局部变量,表示当前线程的主协程,切换到这个协程,就相当于切换到了主线程中运行。

重要成员函数

  • SetThis

    1
    2
    3
    4
    void Fiber::SetThis(Fiber *f)
    {
    t_fiber = f;
    }
    • 作用:表示当前的协程为线程正在执行的协程,即设置线程局部变量 t_fiber
  • GetThis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Fiber::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
      13
      Fiber::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
      20
      Fiber::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,结束后将状态置为 TERMyield 回主协程/调度器。
  • 析构函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    Fiber::~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
  • reset

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void 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;
    }
    • 作用
      • 重置协程状态和入口函数,复用栈空间,不重新创建栈。
  • resume

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    void 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,而不是线程主协程。
  • yield

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    void 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
      • 如果协程参与调度器调度,和调度器的主协程进行切换,让出自己的执行权。
      • 如果协程不参与调度器调度,和线程的主协程进行切换,让出自己的执行权力。
  • MainFunc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void 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,最后让出执行权限。

参考文章:

sylar 源码解析—协程模块

ucontext 学习

从零开始重写sylar C++高性能分布式服务器框架

C++高性能分布式服务器框架