[C++]sylar高性能服务器框架学习记录:协程模块

记录一下最近这学期学习的sylar服务器框架项目,输出整理一下项目的结构,用到的知识和自己的体会

项目仓库地址

https://github.com/sylar-yin/sylar/

整理博客过程中参考的大佬资料链接:

============================================

基础介绍

该模块实现了一个封装了Linux ucontext库的协程类,提供了一系列如初始化,调用,切出等方法(后续配合Scheduler协程调度模块,整合调度器之后可以实现一个N线程:M协程的调度模型,提供高并发能力),采用非对称协程模型(即任何执行实际任务的协程都是从主协程切出的,执行完成之后也必须切回主协程)

什么是协程

可以理解为用户态的线程,由用户负责调度,故不需要OS参与调度,自然也没有来自系统调用的昂贵的用户态/内核态切换成本,协程直接将寄存器上下文和栈保存,来回切换时恢复,速度快,可以使用同步代码实现异步效果,提高并发性能

本模块只定义了一个类Fiber,协程相关功能使用ucontext库,关于ucontext库相关函数与结构体介绍如下:

typedef struct ucontext_t {
 struct ucontext_t *uc_link;    //该上下文指向的下一个上下文
 stack_t uc_stack;              //栈的信息,是一个stack_t结构体
 mcontext_t uc_mcontext;        //保存了上下文各种寄存器信息
 sigset_t uc_sigmask;           //当上下文被激活时屏蔽的信号集
} ucontext_t;

typedef struct {
 void *ss_sp;//栈空间指针,指向栈空间的地址
 int ss_flags;//栈的flags
 size_t ss_size;//栈的大小
} stack_t;

//获取协程上下文
int getcontext(ucontext_t *ucp);

//设置协程上下文
int setcontext(const ucontext_t *ucp);

/*设置协程入口函数,使用前需要配合getcontext()使用,
如果指定了ucontext中的uc_link,那么函数执行完会返回到uc_link执向的上下文*/
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);

//保存当前上下文到oucp,前往ucp执向的上下文,相当于切换上下文
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);

参考了这篇博客

Fiber 协程类

可以看到在Fiber名称空间中定义了枚举State来管理FIber状态,提供了两套构造函数,一套提供给主协程(无栈),一套给子协程(有栈),为Fiber实例提供了与线程主协程切换上下文的方法(call和back),以及另一套与调度器协程切换上下文的方法(swapIn和swapOut),关于调度器模块的内容之后会整合

此外还提供了一些静态方法用来设置协程状态或切换当前协程的上下文;入口函数也进行了封装

成员变量记录了协程ID(从0开始使用原子变量自行递增),栈大小,状态,上下文,栈指针以及任务函数(使用std::function包装)

class Fiber : public std::enable_shared_from_this
<Fiber> {
friend class Scheduler;
public:
    typedef std::shared_ptr
<Fiber> ptr;

    // 协程状态
    enum State {
        INIT,   // 初始化
        HOLD,   // 暂停
        EXEC,   // 执行
        TERM,   // 结束
        READY,  // 可执行
        EXCEPT  // 异常
    };
private:
    // 主协程的构造函数(禁用外部构造)
    Fiber(); 

public:
    // 子协程的构造函数
    Fiber(std::function<void()> cb, size_t stacksize = 0, bool use_caller = false);
    // 析构函数
    ~Fiber();

    // 重置协程(传入新的协程函数)
    void reset(std::function<void()> cb); 

    void swapIn();      // 从调度器协程切换到当前协程
    void swapOut();     // 从当前协程切换到调度器协程

    void call();    // 从主协程切换到当前协程
    void back();    // 从当前协程切换到主协程

    uint64_t getId() const { return m_id; }         // 返回协程ID(类的成员)
    State getState() const { return m_state; }      // 返回协程状态
public:
    // 设置当前运行协程(t_fiber)为*f
    static void SetThis(Fiber* f);
    // 返回当前运行协程(t_fiber),兼具创建线程主协程功能
    static Fiber::ptr GetThis();
    // 协程切换回后台(swapOut),并设置为READY状态
    static void YieldToReady();
    // 协程切换回后台(swapOut),并设置为HOLD状态
    static void YieldToHold();  
    // 返回总协程数(s_fiber_count)
    static uint64_t TotalFibers();

    static void MainFunc();         // 协程入口函数(切回调度器协程)
    static void CallerMainFunc();   // 协程入口函数(切回主协程)
    static uint64_t GetFiberId();   // 返回协程ID(当前运行的协程)  
private:
    uint64_t m_id = 0;              // 协程ID
    uint32_t m_stacksize = 0;       // 协程运行栈大小
    State m_state = INIT;           // 协程状态

    ucontext_t m_ctx;               // 协程上下文
    void* m_stack = nullptr;        // 协程运行栈指针

    std::function<void()> m_cb;     // 协程执行函数
};

全局变量,配置项以及工具类

使用了名为system的全局日志器,sylar所有模块的日志器都叫system,级别为debug

注册了一个配置项来储存协程栈的大小,默认为128KB

声明两个原子变量来统计协程数量

为每个线程声明thread_local变量,记录当前协程和主协程

提供一个对malloc函数封装的内存分配工具类

static Logger::ptr g_logger = SYLAR_LOG_NAME("system");     // 全局日志器

static std::atomic
<uint64_t> s_fiber_id {0};        // 用于生成协程ID
static std::atomic
<uint64_t> s_fiber_count {0};     // 用于统计当前协程数

static thread_local Fiber* t_fiber = nullptr;               // 当前运行协程
static thread_local Fiber::ptr t_threadFiber = nullptr;     // 当前运行协程的主协程

// 协程栈大小配置项
static ConfigVar
<uint32_t>::ptr g_fiber_stack_size = 
    Config::Lookup
<uint32_t>("fiber.stack_size", 128 * 1024, "fiber stack size");

// 用于创建/释放协程运行栈的类
class MallocStackAllocator {
public:
    static void* Alloc(size_t size) {               // 分配栈内存
        return malloc(size);
    }

    static void Dealloc(void* vp, size_t size) {    // 释放栈内存
        return free(vp);
    }
};

using StackAllocator = MallocStackAllocator;        // 分配内存工具类的别名

下面对主要函数的实现进行介绍

构造函数

先来看主协程的构造函数,该构造函数为私有,只会在Fiber类的初始化方法GetThis()中被调用

// 主协程构造函数
Fiber::Fiber() {
    // 设置状态并设置为当前运行协程
    m_state = EXEC;  
    SetThis(this);

    // 获取并保存当前协程的上下文
    if(getcontext(&m_ctx)) {
        SYLAR_ASSERT2(false, "getcontext");
    }

    // 更新协程计数器
    s_fiber_count++;

    // 调试信息
    SYLAR_LOG_DEBUG(g_logger) << "Fiber::Fiber main";
}

然后是子协程的构造函数,子协程才是真正在干活的协程,可以接收任务函数,在外部可以被创建

这里看到的是已经整合了调度器(Scheduler模块)之后的代码,其中use_caller参数是为了适配调度器模块的一个功能添加的

该功能即“是否将调度器本身所在线程也加入调度器线程池”,这个功能会导致出现两种类型的协程:

  • 如果是调度器本身运行的线程上创建的协程,执行完成传入的任务函数之后,应返回的是本身线程的主协程(这个线程运行了调度器,调度逻辑代码写在这个线程的主协程里)
  • 而一般协程,执行完任务函数之后应该返回的“主协程”,即调度器所运行的协程,后续的协程调度逻辑代码写在调度器里,协程运行完成之后返回到调度器就行

为此sylar提供了两种MainFunc形式,根据协程类型来设置MainFunc类型,最终实现不管是调度器线程上的协程,还是普通worker线程上的协程,都能返回到跑这调度逻辑代码的协程

个人认为sylar在这里的逻辑没有处理完美,其实可以统一一种MainFunc,最终在MainFunc内返回时加上判断分支即可,另外也不用设计两套协程切换方法,同样的在方法内部根据协程类型分支即可

如果不追求使用调度器,而只想手动敲代码控制协程的切换,则不用考虑以上的逻辑,只要使用call()/back()切换协程,use_caller填false即可

// 子协程构造函数
Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool use_caller) 
    :m_id(++s_fiber_id)
    ,m_cb(cb) {
    // 更新协程计数器,设置协程栈大小
    ++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;

    // 根据是否参与调度器调度选择并设置协程入口函数
    if(!use_caller) {   // 非调度器协程,执行完毕后返回调度器协程
        makecontext(&m_ctx, &Fiber::MainFunc, 0);
    } else {            // 本身是调度器协程,执行完毕后返回自己的主协程
        makecontext(&m_ctx, &Fiber::CallerMainFunc, 0);
    }

    // 调试信息
    SYLAR_LOG_DEBUG(g_logger) << "Fiber::Fiber id=" << m_id;
}

析构函数

根据不同的协程类型制定不同析构策略

// 析构函数
Fiber::~Fiber() {
    // 更新协程计数器
    --s_fiber_count;
    if(m_stack) {   // 如果是子协程
        SYLAR_ASSERT(m_state == TERM || m_state == INIT || m_state == EXCEPT);
        // 释放栈内存
        StackAllocator::Dealloc(m_stack, m_stacksize);
    } else {        // 如果是主协程
        SYLAR_ASSERT(!m_cb);
        SYLAR_ASSERT(m_state == EXEC);
        // 将当前协程置空
        Fiber* cur = t_fiber;
        if(cur == this) {   // 如果执行中的协程即主协程
            SetThis(nullptr);
        }
    }
    // 调试信息
    SYLAR_LOG_DEBUG(g_logger) << "Fiber::~Fiber id=" << m_id
                              << " total=" << s_fiber_count;
}

协程切换函数

先来看切换回主协程的协程切换函数

// 从协程主协程切换到当前协程
void Fiber::call() {
    // 设置当前协程,设置协程状态
    SetThis(this);
    m_state = EXEC; 
    // 切换上下文(自主协程)
    if(swapcontext(&t_threadFiber->m_ctx, &m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}

// 从当前协程切换到协程主协程
void Fiber::back() {
    // 设置当前协程
    SetThis(t_threadFiber.get());
    // 切换上下文(到主协程)
    if(swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}

然后是整合了调度器模块之后添加的另一套用于于调度器协程切换的函数(需要配合调度器使用,不然没有调度器实例会段错误,虽然有了调度器之后也不会手动用这些函数就是了)

// 从调度器主协程切换到当前协程
void Fiber::swapIn() {
    // 设置当前协程,设置协程状态
    SetThis(this);
    SYLAR_ASSERT(m_state != EXEC);
    m_state = EXEC;
    // 切换上下文(自调度器协程)
    if(swapcontext(&Scheduler::GetMainFiber()->m_ctx, &m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}
// 从当前协程切换到调度器主协程
void Fiber::swapOut() {
    // 设置当前协程
    SetThis(Scheduler::GetMainFiber());
    // 切换上下文(到调度器协程)
    if(swapcontext(&m_ctx, &Scheduler::GetMainFiber()->m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}

主协程初始化函数

静态方法,调用后会创建线程的主协程(如果没有),并返回当前运行的协程

个人认为应该把这两个功能分成两个函数

// 返回当前运行协程(t_fiber),兼具创建线程主协程功能
Fiber::ptr Fiber::GetThis() {
    // 如果t_fiber存在(意味主协程已经构造)
    if(t_fiber) {
        return t_fiber->shared_from_this();
    }
    // 否则构造主协程
    Fiber::ptr main_fiber(new Fiber);
    SYLAR_ASSERT(t_fiber == main_fiber.get());
    t_threadFiber = main_fiber;
    return t_fiber->shared_from_this();
}

调度器相关的静态函数

主要给调度器使用(或者把swapOut改成back,这样没有调度器也能用)

个人这里其实和上面一样,根据协程类型分支一下更好,所有类型的Fiber都可以用

// 协程切换回调度器后台,并设置为READY状态
void Fiber::YieldToReady() {
    Fiber::ptr cur = GetThis();
    SYLAR_ASSERT(cur->m_state == EXEC);
    cur->m_state = READY;
    // 调度器接管
    cur->swapOut();
}
// 协程切换回调度器后台,并设置为HOLD状态
void Fiber::YieldToHold() {
    Fiber::ptr cur = GetThis();
    SYLAR_ASSERT(cur->m_state == EXEC);
    // cur->m_state = HOLD;
    // 调度器接管
    cur->swapOut();
}

MainFunc协程入口函数

传给协程的任务函数运行在MainFunc中,执行完成之后调用协程切换函数,可以看到两种协程的两套MainFunc只有协程切换语句不同其他一模一样(疑似sylar偷懒

这里最后协程执行完毕后不能直接用自己的智能指针返回,是怕back之后智能指针引用一直在,导致Fiber不能析构造成内存泄漏,所以选择手动释放智能指针所有权让析构函数运行,再使用裸指针调用back

// 协程入口函数
void Fiber::MainFunc() {
    Fiber::ptr cur = GetThis();
    SYLAR_ASSERT(cur);
    // 执行协程函数,执行完毕后置空,并设置状态
    try {
        cur->m_cb();
        cur->m_cb = nullptr;
        cur->m_state = TERM;
    } catch (std::exception& ex) {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except: " << ex.what()
            << " fiber_id=" << cur->getId()
            << std::endl
            << sylar::BacktraceToString();
    } catch (...) {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except"
            << " fiber_id=" << cur->getId()
            << std::endl
            << sylar::BacktraceToString();
    }

    // 退出协程,调度器接管
    auto raw_ptr = cur.get();   // 获取当前协程裸指针
    cur.reset();                // 将智能指针置空,引用-1,执行析构
    raw_ptr->swapOut();         // 切换回调度器协程

    SYLAR_ASSERT2(false, "never reach fiber_id=" + std::to_string(raw_ptr->getId()));
}

// 协程入口函数
void Fiber::CallerMainFunc() {
    Fiber::ptr cur = GetThis();
    SYLAR_ASSERT(cur);
    try {
        cur->m_cb();
        cur->m_cb = nullptr;
        cur->m_state = TERM;
    } catch (std::exception& ex) {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except: " << ex.what()
            << " fiber_id=" << cur->getId()
            << std::endl
            << sylar::BacktraceToString();
    } catch (...) {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except"
            << " fiber_id=" << cur->getId()
            << std::endl
            << sylar::BacktraceToString();
    }

    // 退出协程,主协程接管
    auto raw_ptr = cur.get();   // 获取当前协程裸指针
    cur.reset();                // 将智能指针置空,引用-1,执行析构
    raw_ptr->back();            // 切换回主协程
    SYLAR_ASSERT2(false, "never reach fiber_id=" + std::to_string(raw_ptr->getId()));
}

断言宏

sylar在模块的实现中大量使用了自己定义的断言宏SYLAR_ASSERT,下面是定义和实现,其中UNLIKELY宏是告诉编译器条件几乎不可能成立,让编译器优化性能

#ifndef __SYLAR_MACRO_H__
#define __SYLAR_MACRO_H__

#include <assert.h>
#include <string.h>
#include "util.h"

#if defined __GNUC__ || defined __llvm__
#   define SYLAR_LIKELY(x)      __builtin_expect(!!(x), 1)
#   define SYLAR_UNLIKELY(x)      __builtin_expect(!!(x), 0)
#else
#   define SYLAR_LIKELY(x)          (x)
#   define SYLAR_UNLIKELY(x)          (x)
#endif

#define SYLAR_ASSERT(x) \
    if(SYLAR_UNLIKELY(!(x))) { \
        SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ASSERTION: " #x \
            << "\nbacktrace:\n" \
            << sylar::BacktraceToString(100, 2, "    "); \
        assert(x); \
    }

#define SYLAR_ASSERT2(x, w) \
    if(SYLAR_UNLIKELY(!(x))) { \
        SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ASSERTION: " #x \
            << "\n" << w \
            << "\nbacktrace:\n" \
            << sylar::BacktraceToString(100, 2, "    "); \
        assert(x); \
    }

#endif // __SYLAR_MACRO_H__

sylar::BacktraceToString()函数是sylar实现的用于打印调用栈的工具函数,使用了backtrace函数,加上递归

void Backtrace(std::vector<std::string>& bt, int size, int skip) {
    void** array = (void **)malloc(sizeof(void*) * size);
    size_t s = ::backtrace(array, size);

    char** strings = backtrace_symbols(array, s);
    if(strings == NULL) {
        SYLAR_LOG_ERROR(g_logger) << "backtrace_symbols error";
        return;
    }

    for(size_t i = skip; i < s; i++) {
        bt.push_back(strings[i]);
    }

    free(strings);
    free(array);
}

std::string BacktraceToString(int size, int skip, const std::string& prefix) {
    std::vector<std::string> bt;
    Backtrace(bt, size, skip);
    std::stringstream ss;
    for(size_t i = 0; i < bt.size(); i++) {
        ss << prefix << bt[i] << std::endl;
    }
    return ss.str();
}

测试

这里只测试手动切换协程

#include "sylar/sylar.h"

sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();

void run_in_fiber() {
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber begin";
    sylar::Fiber::GetThis()->back();
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber end";
    sylar::Fiber::GetThis()->back();
}

void test_fiber() {
    SYLAR_LOG_INFO(g_logger) << "main begin -1";
    {
        sylar::Fiber::GetThis();
        SYLAR_LOG_INFO(g_logger) << "main begin";
        sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber));    // 创建子协程
        fiber->call();                                              // 调用
        SYLAR_LOG_INFO(g_logger) << "main after swapIn";
        fiber->call();
        SYLAR_LOG_INFO(g_logger) << "main after end";
    }
    SYLAR_LOG_INFO(g_logger) << "main after end2";
}

int main(int argc, char** argv) {
    sylar::Thread::SetName("main");

    std::vector<sylar::Thread::ptr> thrs;
    for(int i = 0; i < 3; i++) {
        thrs.push_back(sylar::Thread::ptr(new sylar::Thread(&test_fiber, "name" + std::to_string(i))));
    }
    for(auto i : thrs) {
        i->join();
    }

    SYLAR_LOG_INFO(g_logger) << "main after end2";
    return 0;
} 

可以看到程序行为符合预期

image-20260206205532322

总结

sylar通过ucontext库实现了非对称协程,配合后续的调度模块以及整合epoll的IO协程调度模块和IOHook模块,能可靠地实现服务器高并发

评论

  1. Sankkooos
    Android Firefox 134.0
    18 小时前
    2026-2-07 0:39:16

    ദ്ദി˶>𖥦<)✧

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇