[源码阅读]——Sylar服务器框架:协程模块

协程概念

  按照本人简单的理解,协程可以看成是一个轻量级的线程,或者是可以切换出去的函数。相比之下本人认为其和函数更像,只是在程序中,如果我们在函数fun()中执行函数test(),则是test()必须执行完毕后,才会返回fun()继续执行。而对于协程来说,其可以执行一半退出,让出cpu执行权。同样,当满足其执行要求时,其会从退出的地方继续执行,又获得了CPU的使用权。所以也可以将其理解成一个轻量级的线程。但和线程不同的是,协程是完全在用户态执行的,而且一个线程可以有多个协程,但是这些协程若都是在一个线程上运行,则其都是在同一个cpu上运行,是无法使用cpu的多核能力的,也就是说虽然协程可以进行上下文切换,但其实际都是串行执行。
  相对于C++,协程在GO语言的运用好像是更为广泛的(本人没有了解过GO语言,只是查阅过一些资料),其应该是内置了协程特性。在C++中,关于开源的协程库可以阅读微信开源的Libco,其类似于pthread的接口设计。sylar的协程模块设计感觉和libco类似,都使用了非对称协程模型。

  • 非对称协程模型:非对称协程本人的理解更类似于函数的之间的调用,也就是说一个协程只会跟调用它的协程绑定,当其让出CPU执行权的时,只会返回原调用者。举个例子,比如我们现在有三个协程cb_1,cb_2,cb_3,首先是cb_1协程执行,随后在cb_1中调用cb_2,然后在cb_2中调用cb_3,此时cb_3让出cpu执行权时,只能返回到cb_2中,而是无法直接返回cb_1的,而cb_2让出执行权时,只能返回给cb_1,也就是说两个协程之间是具有类似的“父子”关系的。
  • 对称协程模型:GO语言所提供的协程,是典型的对称型协程。根据本人的理解就是协程之间是对等的,是可以转移给任意一个协程的,因此在对称式协程切换时,需要明确指明另外一个协程获得调度权。但是如此看来对称协程的实现相对于非对称要困难一些,非对称协程只需要保存调用自己的“父协程”的上下文信息即可,而对称式携程则需要自己充当调度器,寻找出合适的协程进行切换,呢么整个流程就会比较麻烦难以管理。
  • 一种比较常见的方法是:使用非对称协程实现对称协程模型,本人的理解是需要专门的调度模块去调度协程之间的切换。举个例子,比如限制我们有两个协程cb_1和cb_2,同时有一个调度的协程为sch,则若此时cb_1在运行,想要切换到cb_2,其流程为:cb_1让出cpu,返回调度协程sch,sch进行调度,cb_2开始执行,当cb_2执行完毕后返回sch,sch根据实际判断是否要调度cb_1继续开始执行。简单来看就是每次协程让出CPU执行权时,无法直接和希望运行的协程进行切换,而是必须要经过调度模块进行调度。(本人比较简单的理解,如果有错误还希望大家及时指出)

  在sylar的携程模块设计和调度模块设计中,使用的便类似于上述的使用非对称协程模型设计从而实现对称的效果。

sylar协程模块

  在sylar的协程模块设计中,首先对协程的六个状态进行了设定,其定义了一个枚举量如下;

// 协程状态
enum State {
    INIT,       // 初始化状态
    HOLD,       // 暂停状态
    EXEC,       // 执行中状态
    TERM,       // 结束状态
    READY,      // 可执行状态
    EXCEPT      // 异常状态
};

  同时定义了其基本的成员变量,并进行了初始化:

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;     // 协程运行函数

  在"fiber.cc"文件中,还声明了几个静态变量,其中关于协程数、协程id等,使用了原子操作,避免了多线程竞争问题。

// 全局静态变量,用于生成协程id
static std::atomic<uint64_t> s_fiber_id {0};
// 全局静态变量,用于统计当前的协程数
static std::atomic<uint64_t> s_fiber_count {0};

// 线程局部变量,当前线程正在运行的协程
// 用于保存当前正在运行的写成指针,必须时刻指向当前正在运行的协程对象
// 协程模块初始化时指向主协程对象
static thread_local Fiber* t_fiber = nullptr;
// 线程局部变量,当前线程的主要协程,切换到这个协程即切换到主线程运行
// 协程模块初始化时,指向线程主协程对象
// 当切换到子协程执行时,通过swapcontext将主协程的上下文保存到t_thread_fiber的ucontext_t成员中,激活子协程上下文
// 当子协程切换到后台时,取出主协程的上下文并恢复运行
static thread_local Fiber::ptr t_threadFiber = nullptr;

  同时,sylar还对malloc进行了重新封装

// 重新封装malloc
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类的构造函数:

  • 其中Fiber()为无参构造,其属于Fiber的私有类,只会在GetThis()方法中次啊会进行调用,用于创建线程的第一个协程,即主函数对应的协程。这也就导致GetThis()同样具有一定初始化主协程的功能,在有些时候希望调用某些函数时,就必须先调用一次GetThis()。这里在后面"fiber_test.cc"中会单独的提到。
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";
}
  • 有参构造则是用于创建用户协程,其中参数use_caller表示是否在主协程上进行调度。
// 用于创建用户协程
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;

    // 是否在MainFiber上调度
    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;
}

  基于if(!use_caller)可以看到,是否在主线程调度的区别,此时可以深究Fiber::MainFunc()Fiber::CallerMainFunc()的区别:

// 协程入口函数
void Fiber::MainFunc() {
    SYLAR_LOG_DEBUG(g_logger) << "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();
    raw_ptr->swapOut();

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

void Fiber::CallerMainFunc() {
    SYLAR_LOG_DEBUG(g_logger) << "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();
    raw_ptr->back();
    SYLAR_ASSERT2(false, "never reach fiber_id=" + std::to_string(raw_ptr->getId()));
}

  这两个函数其实看起来很相似,主要区别在于raw_ptr->swapOut();raw_ptr->back(),此时可以继续探究一下两个方法的区别:

void Fiber::back() {
    SetThis(t_threadFiber.get());
    if(swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}
void Fiber::swapOut() {
    SetThis(Scheduler::GetMainFiber());
    if(swapcontext(&m_ctx, &Scheduler::GetMainFiber()->m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}

  上述两个函数主要是在swapcountext处有所出入,只是一个是返回其“父协程”,一个则是返回调度协程。所以本人认为sylar在此是有些将问题复杂化了,感觉可以直接用一个yield方法去处理这个情况,即:

void Fiber::yield() {
    SetThis(Scheduler::GetMainFiber());
    if(use_caller){
        if(swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
            SYLAR_ASSERT2(false, "swapcontext");
        }
    }
    else{
        if(swapcontext(&m_ctx, &Scheduler::GetMainFiber()->m_ctx)) {
                SYLAR_ASSERT2(false, "swapcontext");
        }
    }
}

  与之相同的还有swapIn()call()方法,但是具体将其整理进去还没有尝试,私认为可以在Fiber类中加一个m_use_call成员变量,从而控制是否需要主线程进行调度,在初始化时令m_use_call = use_call

  • 随后是协程的析构,主要是协程总数减一、销毁协程内存。当然在销毁之前要判断协程状态,如是否异常等。
  • 协程重置函数即利用已结束的协程空间,创建新的协程,类似于其有参构造。
  • 此外还有两个比较重点的函数是YieldToReadyYieldToHold,其分别是将协程切换到后台并设置为ready状态或hold状态。但这里出现一个奇怪的现象就是本人发现sylar后面其方法几乎都是使用的YieldToHold,并且在调用之前协程自己会将要自己放入调度队列中,所以在此感觉ready和hold两种状态其实是可以合并的。也就是说每个协程在让出CPU执行权时,若后续还需要执行,则要自己主动将自己加入到调度队列中,而非管理者再去判断是否要重新加入。

其他

  关于协程模块整体来看其实代码量没有很大,可能还需要结合后面的调度模块详细研究。但是感觉sylar在设计协程的时候可能没有进一步精简,个人感觉可以再适当整合一下。
  此外还有一个问题值得注意,就是上文说的"fiber_test.cc"中的内容,在sylar原始的程序中,其让出执行权如下:

void run_in_fiber() {
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber begin";
    sylar::Fiber::YieldToHold();
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber end";
    sylar::Fiber::YieldToHold();
}

  但是细究可以发现,YieldToHold()调用的是swapOut(),其让出执行权后是返回调度协程,会导致报错,因此需要将其修改为:

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

  当然,这里主要是和后面的调度模块相结合了,如果跟着视频去测试的话并没有什么问题。
  后面就是要结合协程调度模块去学习,可能本人理解能力有限,关于协程很多地方没有详细探究,而且调度模块还是一知半解状态,有需要的可以多去看看源码和其他大佬的笔记~如果有些地方描述或理解有问题,也欢迎大家指正。


sylar C++ 高性能服务器(项目地址)
sylar个人主页

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甄姬、巴豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值