Skip to content

RoutineLi/libgo-simple

Repository files navigation

libgo-simple

一个简单的go风格的协程库基于c++11和libco,包括hook机制和协程原语设计.

 使用方法:
 IOManager loop(1);  //定义调度器
 go func; //开启一个协程运行func函数
 go[]()mutable{
   //TODO
 }; //lambda
 int a = 1;
 Go{
  a = 2;
 }; //大写Go默认捕获全部局部变量,默认按值捕获
 
 
 /**
  * 基于Channel的简易消费者生产者模型
  **/
  Channel<int> chan(1);
  Go{
      for(int i = 0; i < 10; ++i){
          chan << i;
          ROUTN_LOG_INFO(g_root) << "send: " << i;
      }
      chan.close();
  };

  Go{
      int i;

      while(chan >> i){
          ROUTN_LOG_INFO(g_root) << "recv: " << i;
      }
  };

1.协程实现

​ 协程(co-routine),又名纤程(fiber),是一种用户态下的轻量级线程,传统Linux操作系统下的线程是Posix实现,本质是通过内核态,由内核cpu调度各个线程的上下文切换,在高并发的情况下,频繁的上下文切换会产生大量的从内核态到用户态的转换从而浪费大量的CPU时间,进而降低程序的性能,但协程的上下文切换是发生在用户态的,内核是感知不到的,整个过程并不会涉及到用户态到内核态的陷入,从而使性能得到巨大的提升。目前协程分为有栈协程和无栈协程,按照实现又可以分为对称和非对称协程;有栈协程实现较为简单,本协程库实现采用有栈(栈空间大小128k)非对称协程模型。

​ 本协程库基于上下文切换实现的方式,源码通过条件编译的方式实现了两种方式的上下文,一种采用POSIX接口的ucontext实现,另一种采用腾讯开源的LibCo的SwapCtx汇编实现上下文切换。

2.IO调度模块

​ 本协程库采用1:N:M的调度模型,即一个进程对应N个线程,一个线程对应M个协程的调度模型,使用Linux的Epoll路复用和管道机制来调度处理各个IO事件。

大致流程:
  1.  IOManager iom --> IOManager() --> Schedular() --> Fiber() --> schedular.start() 创建io协程调度器实例会初始化线程和线程的协程,此时共有一个线程一个协程
  2.  iom.schedule(callback()) --> iom.tickle() -->                                   有回调函数或者有协程实例注册绑定回调函数的协程到协程消息队列中(+1)
  3.  ~IOManager() --> Schedular::stop() --> Fiber::call() --> run()                  在io协程调取器析构时会先执行基类的stop()此方法才是真正开始执行调度逻辑的接口
  4.  -->swapIn() --> Fiber::MainFunc() --> callback() --> addEvent(func)                 run()函数中循环判断该协程绑定的有回调函数则resume该协程并在协程的MainFunc()执行回调函数
  5.  -->swapOut() --> run()                                                          回调函数中注册事件此时开始执行epoll逻辑,注册完成后返回MainFunc()执行swapOut()回到run
  6.  -->idle()--> IOManager::FdContext::triggerEvent() --> func()                    run循环判断idle协程状态并执行,idle协程执行idle()并检测定时器事件有无触发并在其中等待epoll_wait()后通过triggerEvent()触发事件

            [Fiber]                    [Timer]
                ^  N                      ^
                |                         |
                | 1                       |
            [Thread]                [TimerManager]
                ^  M                      ^
                |                         |
                | 1                       |
            [Schedular] <---------[IOManager(epoll)]

2.定时器模块设计

​ 本协程库的定时器设计基于最小堆,本质就是每次从定时器Set中取出一个绝对时间最小的来与当前绝对时间判断,若

超时则执行,支持最高精度Ms级(Epoll最高支持ms级), 支持添加,删除,条件触发,循环触发定时器事件。实现较为简单。

​ 定时事件依赖于Linux提供的定时机制,目前Linux提供了以下几种可供我们使用的机制:

1. 套接字超时选项,SO_RCVTIMEO和SO_SNDTIMEO,通过errno判断超时
2. alarm()和settimer(),通过SIGALRAM信号触发,通过sighandler捕获信号判断超时
3. IO多路复用epoll的超时选项
4. timerfd_create()和timer_create()系列接口

​ 本定时器基于epoll_wait超时机制,IO调度器会在调度器空闲时阻塞在执行epoll_wait()的协程上,等待IO事件发生,epoll_wait()的超时依赖于当前定时器的最小超时时间,每次epoll_wait()返回后都会根据当前的绝对时间把已经超时的定时器事件收集起来,执行它们的回调函数,由于epoll_wait()的返回值不能绝对判断是否超时,因此每次返回后都要再判断一下定时器有没有超时,这时通过比较当前绝对时间和定时器的绝对超时时间,就可以确定一个定时器是否超时。

​ 由于本定时器采用gettimeofday()获取绝对时间,所以依赖于系统时间,如果系统时间被修改或者校时,那么这套定时机制就会失效,目前解决方法是每次默认设置一个较小的epoll_wait()超时时间,比如3s,假设有一个定时任务超时时间是10s以后,那么epoll_wait()要超时三次才能触发,每次超时后还要检查一下系统时间是否被回调过,如果被回调一个小时以上,则默认全部超时(这个办法还比较暴力,更好的解决办法是换一个时间源,改为clock_gettime(CLOCK_MONOTONIC_RAW, &ts)获取单调时间更好一点)。

3.HOOK 机制

​ 由于协程是用户态下的线程,在日常业务处理中,一旦协程所在的函数有阻塞,那么整个协程所在的线程都会阻塞在这里,这会大大降低应用协程的性能,甚至会发生意想不到的重大事故,例如死锁。因此在异步化改造过程中使用原版的各种系统调用像是accept(), connnect(), sleep()等等都会产生陷入内核态从而导致阻塞,那该怎么办呢?聪明的程序员想到了一个办法,通过将各种会产生阻塞的系统调用做一个hook,将原版系统调用替换一下,让代码执行我们想要的非阻塞操作不就好了吗,例如sleep()函数休眠5s,正常情况下调用线程会陷入内核态阻塞住5s,但hook后,线程并不会真的阻塞而是让协程添加一个5s的定时器任务,然后将该协程yield让出执行权,线程上的其它协程继续执行,等到5s后定时器触发,再接着执行原本的上下文。这样子给人感觉像是阻塞住了,实际上通过定时器巧妙的实现了和原版系统调用相同的效果并且没有阻塞线程,这就是Hook模块的作用。

4.协程间同步原语,协程间通信

​ 我们都知道,一旦协程阻塞后整个协程所在的线程都将阻塞住,这也就失去了协程的优势。而随着业务的升级迭代,协程间同步甚至协程间通信的概念便被提出,原本的LibCo并不支持协程同步原语,而Go语言之所以能再短短几年称霸后端语言一方,迅速占有市场,不但因为其内部完整实现了Go协程——goroutine的同步机制,还实现了强大的协程间通信——Channel。

​ 然而Linux常见的提供给我们的同步原语和互斥量都会发生阻塞,因此需要重新实现一套用户态协程同步原语才能解决问题。

​ 如同协程比于线程,我们很容易得到一个启示,既然内核维护等待队列会阻塞线程,那可以用户态自己维护,当获取协程同步对象失败时,用户将条件不满足的协程放入队列,然后Yield让出控制权,等待条件满足时再将协程重新从队列取出加入调度器继续执行,与HOOK模块的原理非常相似!

​	1.FiberMutex: 协程锁,基于Linux POSIX的spinlock(),选用自旋锁是因为不论是TAS实现还是POSIX,它们的自旋锁都不会阻塞线程,如果获取锁失败,就把当前协程放入队列中,并让出执行权。

​	2.FiberCond: 条件变量,和Mutex一样,维护了一个等待队列

​	3.FiberSem: 信号量,基于条件变量和互斥量

​	4.Channel信道,主要用于协程间通信,在实现上与锁一样,采用了PIMPL设计模式,具体操作转发给实现类,本质是一个队列,一边从队列写数据,另一边从队列消费数据

About

一个简单的go风格的协程库基于c++11和libco,包括hook机制和协程原语设计

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors