我是靠谱客的博主 风趣水池,这篇文章主要介绍[源码阅读]——Sylar服务器框架:协程模块,现在分享给大家,希望可以做个参考。

协程模块

    • 协程概念
    • 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的协程模块设计中,首先对协程的六个状态进行了设定,其定义了一个枚举量如下;

复制代码
1
2
3
4
5
6
7
8
9
10
// 协程状态 enum State { INIT, // 初始化状态 HOLD, // 暂停状态 EXEC, // 执行中状态 TERM, // 结束状态 READY, // 可执行状态 EXCEPT // 异常状态 };

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

复制代码
1
2
3
4
5
6
7
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等,使用了原子操作,避免了多线程竞争问题。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全局静态变量,用于生成协程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进行了重新封装

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 重新封装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"中会单独的提到。
复制代码
1
2
3
4
5
6
7
8
9
10
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表示是否在主协程上进行调度。
复制代码
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
// 用于创建用户协程 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()的区别:

复制代码
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 协程入口函数 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(),此时可以继续探究一下两个方法的区别:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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方法去处理这个情况,即:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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原始的程序中,其让出执行权如下:

复制代码
1
2
3
4
5
6
7
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(),其让出执行权后是返回调度协程,会导致报错,因此需要将其修改为:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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个人主页

最后

以上就是风趣水池最近收集整理的关于[源码阅读]——Sylar服务器框架:协程模块的全部内容,更多相关[源码阅读]——Sylar服务器框架内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(79)

评论列表共有 0 条评论

立即
投稿
返回
顶部