libgo框架介绍

是专为Linux服务端程序开发设计的底层框架

提示:本文尚有许多细节还未曾说到:如:时间钟,以及等待队列等,本文可以先对协程框架有个大概的了解,后续会的文章将会对更多的细节进行更新


一.官方介绍

官方地址
使用libgo编写并行程序,即可以像golang、erlang这些并发语言一样开发迅速且逻辑简洁,又有C++原生的性能优势,鱼和熊掌从此可以兼得。

二.目录介绍

  • TODO:libgo 后续会逐步完善或增加的功能;
  • libgo:源码实现的主目录,关于协程和调度策略的实现都在该目录下;
  • test:测试代码;
  • tutorial:libgo 使用教程代码;
  • vs_proj:VS 环境下如何使用libgo。
  • libgo目录下:
    • task:协程的相关实现;
    • scheduler:协程调度的实现;
    • debug:libgo 自带的调试功能(用于协程状态的定位等);
    • coroutine.h:对一些常用对方法进行了重定义。
  • netio:hook的系统调用;
  • context:上下文的切换;
  • pool:libgo 实现的连接池

三.调度原理及其机制概述

  • 每个任务是通过任务调度器进行切换来实现协程,任务调度器分为调度器和执行器
  • 当创建协程时,会将协程添加到某一个处于活跃状态的执行器,如果恰好都不活跃,也会添加到某一个执行器中,这并不影响执行器的正常工作,因为调度器的调度线程会去处理它;

1.调度器

  • 调度器管理执行器,调度器主要的工作时平衡每个执行器之间的工作,防止某一个执行器过忙或者没有任务
  • 当某一个执行器卡住时,调度器也会将该执行器中的协程取出,放到负载最低的执行器上面。
  • 所有的执行器都会在一个双端队列中
  • 当调用了 Start() 函数后,会正式开始运行
  • 调度线程会平衡各个 P 的协程数量和负载,进行 steal
  • 如果所有 P 都阻塞,会根据 maxThreadNumber 动态增加 P 的数量
  • 如果仅仅部分 P 阻塞,会将阻塞的 P 中的协程全部拿出(steal),均摊到负载最小的 P 中
class Scheduler
{
   public:
//        typedef TSQueue<Task> TaskList;  // 线程安全的协程队列
       typedef TSSkipQueue<Task> TaskList;  // 线程安全的协程队列
       typedef TSQueue<Processer> ProcList;
       typedef std::pair<uint32_t, TSQueue<Task, false>> WaitPair;
       typedef std::unordered_map<uint64_t, WaitPair> WaitZone;
       typedef std::unordered_map<int64_t, WaitZone> WaitTable;

       static Scheduler& getInstance();

       // 获取配置选项
       CoroutineOptions& GetOptions();

       // 创建一个协程
       void CreateTask(TaskF const& fn, std::size_t stack_size = 0);

       // 当前是否处于协程中
       bool IsCoroutine();

       // 是否没有协程可执行
       bool IsEmpty();

       // 当前协程让出执行权
       void CoYield();

       // 调度器调度函数, 内部执行协程、调度协程
       uint32_t Run();

       // 循环Run直到没有协程为止
       void RunUntilNoTask();
       
       // 无限循环执行Run
       void RunLoop();

       // 当前协程总数量
       uint32_t TaskCount();

       // 当前协程ID, ID从1开始(不在协程中则返回0)
       uint64_t GetCurrentTaskID();

       // 当前协程切换的次数
       uint64_t GetCurrentTaskYieldCount();

       // 设置当前协程调试信息, 打印调试信息时将回显
       void SetCurrentTaskDebugInfo(std::string const& info);

       // 获取当前协程的调试信息, 返回的内容包括用户自定义的信息和协程ID
       const char* GetCurrentTaskDebugInfo();

       // 获取当前线程ID.(按执行调度器调度的顺序计)
       uint32_t GetCurrentThreadID();

   public:
       /// sleep switch
       //  \timeout_ms min value is 0.
       void SleepSwitch(int timeout_ms);

       /// ------------------------------------------------------------------------
       // @{ 定时器
       TimerId ExpireAt(CoTimerMgr::TimePoint const& time_point, CoTimer::fn_t const& fn);

       template <typename Duration>
       TimerId ExpireAt(Duration const& duration, CoTimer::fn_t const& fn)
       {
           return this->ExpireAt(CoTimerMgr::Now() + duration, fn);
       }

       bool CancelTimer(TimerId timer_id);
       bool BlockCancelTimer(TimerId timer_id);
       // }@
       /// ------------------------------------------------------------------------
   
       /// ------------------------------------------------------------------------
       // @{ 线程池
       ThreadPool& GetThreadPool();
       // }@
       /// ------------------------------------------------------------------------

   public:
       Task* GetCurrentTask();

       /// 调用阻塞式网络IO时, 将当前协程加入等待队列中, socket加入epoll中.
       void IOBlockSwitch(int fd, uint32_t event, int timeout_ms);
       void IOBlockSwitch(std::vector<FdStruct> && fdsts, int timeout_ms);

   private:
       Scheduler();
       ~Scheduler();

       Scheduler(Scheduler const&) = delete;
       Scheduler(Scheduler &&) = delete;
       Scheduler& operator=(Scheduler const&) = delete;
       Scheduler& operator=(Scheduler &&) = delete;

       // 将一个协程加入可执行队列中
       void AddTaskRunnable(Task* tk);

       // Run函数的一部分, 处理runnable状态的协程
       uint32_t DoRunnable();

       // Run函数的一部分, 处理epoll相关
       int DoEpoll();

       // Run函数的一部分, 处理sleep相关
       uint32_t DoSleep();

       // Run函数的一部分, 处理定时器
       uint32_t DoTimer();

       // 获取线程局部信息
       ThreadLocalInfo& GetLocalInfo();

       // List of Processer
       LFLock proc_init_lock_;
       uint32_t proc_count = 0;
       ProcList run_proc_list_;
       ProcList wait_proc_list_;

       // List of task.
       TaskList run_tasks_;

       // io block waiter.
       IoWait io_wait_;

       // sleep block waiter.
       SleepWait sleep_wait_;

       // Timer manager.
       CoTimerMgr timer_mgr_;

       ThreadPool *thread_pool_;

       std::atomic<uint32_t> task_count_{0};
       std::atomic<uint8_t> sleep_ms_{0};
       std::atomic<uint32_t> thread_id_{0};

   private:
       friend class CoMutex;
       friend class BlockObject;
       friend class IoWait;
       friend class SleepWait;
       friend class Processer;
};

}

2.执行器

  • 每个执行器是一个单独的线程
  • 当一个协程阻塞时就会切换上下文,切入到一个准备就绪的上下文去继续处理。
  • 没有可执行的协程时就会阻塞等待。
  • 支持动态增长,且数目不超过maxThreadNumbe,不少于minThreadNumber
  • 初始是创建minThreadNumber个,最大不超过maxThreadNumber
  • 主线程也是一个调度器
  • 只要一个协程队列,没有等待队列,等待队列在调度器里
#pragma once
#include "task.h"
#include "ts_queue.h"

namespace co {

struct ThreadLocalInfo;

//实现执行器的双端队列
struct TSQueueHook
{
    TSQueueHook *prev = NULL;
    TSQueueHook *next = NULL;
    void *check_ = NULL;
};
// 协程执行器
//   管理一批协程的共享栈和调度, 非线程安全.
class Processer : public TSQueueHook
{
private:
    // 任务队列
    typedef TSQueue<Task> TaskList;

    // 线程id
    uint32_t id_;
    // 存储共享栈
    char *shared_stack_ = NULL;
    // 用于存储共享栈的大小
    uint32_t shared_stack_cap_ = 0;
    // 定任务数量
    std::atomic<uint32_t> task_count_{0};
    // 可运行的任务
    TaskList runnable_list_;

    // 线程id从0开始,第一个线程是1,用于设置线程id,atomic保证了原子性
    static std::atomic<uint32_t> s_id_;

public:
    //初始化进程的ID和共享栈的大小
    explicit Processer(uint32_t stack_size);

   //用于释放共享栈的内存
    ~Processer();

    // 添加任务到可运行列表,task_count_++,不添加异常状态的协程,
    void AddTaskRunnable(Task *tk);

    // 执行任务
    uint32_t Run(ThreadLocalInfo &info, uint32_t &done_count);

    // 协程切换
    void CoYield(ThreadLocalInfo &info);

    // 获取任务数量
    uint32_t GetTaskCount();
};

} //namespace co

  • run函数中通过取出当前协程队列runable_list_中的所有队列到slist中进行遍历执行,执行结束后通过对task状态判断,进行处理
    • runnable:执行下一个task
    • IO阻塞:从slist任务队列删除,放到调度器的IO等待队列
    • sleep :从slist任务队列删除,放到调度器的睡眠等待队列
    • 系统阻塞:从slist任务队列删除,再次放到runnable_list(不仅如此,还有一点细节不清楚)
    • 异常:从slist任务队列删除,再次放到runnable_list
    • 完成状态:// 从链表中移除该任务
uint32_t Processer::Run(ThreadLocalInfo &info, uint32_t &done_count)
{
   // 加锁,保证线程安全
   ContextScopedGuard guard;

   // 初始化当前任务和完成任务数量
   info.current_task = NULL;
   done_count = 0;
   uint32_t c = 0;
   // 从可运行任务列表中取出所有任务
   SList<Task> slist = runnable_list_.pop_all();
   // 获取任务数量
   uint32_t do_count = slist.size();

   // 打印调试信息
   DebugPrint(dbg_scheduler, "Run [Proc(%d) do_count:%u] --------------------------", id_, do_count);

   // 遍历任务列表
   SList<Task>::iterator it = slist.begin();
   for (; it != slist.end(); ++c)
   {
       // 获取任务
       Task* tk = &*it;
       // 设置当前任务
       info.current_task = tk;
       // 设置任务状态为可运行
       tk->state_ = TaskState::runnable;
       // 打印调试信息
       DebugPrint(dbg_switch, "enter task(%s)", tk->DebugInfo());
       // 执行任务
       if (!tk->SwapIn()) {
           // 打印错误信息
           fprintf(stderr, "swapcontext error:%s\n", strerror(errno));
           // 将任务重新放入可运行任务列表
           runnable_list_.push(tk);
           // 抛出错误
           ThrowError(eCoErrorCode::ec_swapcontext_failed);
       }
       // 打印调试信息
       DebugPrint(dbg_switch, "leave task(%s) state=%d", tk->DebugInfo(), tk->state_);
       // 设置当前任务为空
       info.current_task = NULL;

       switch (tk->state_) {
           // 任务状态为可运行
           case TaskState::runnable:
               // 遍历器后移
               ++it;
               break;

           // 任务状态为IO阻塞
           case TaskState::io_block:
               // 从链表中移除该任务
               it = slist.erase(it);
               // 将任务切换到IO等待队列
               g_Scheduler.io_wait_.SchedulerSwitch(tk);
               break;

           // 任务状态为睡眠
           case TaskState::sleep:
               // 从链表中移除该任务
               it = slist.erase(it);
               // 将任务切换到睡眠等待队列
               g_Scheduler.sleep_wait_.SchedulerSwitch(tk);
               break;

           // 任务状态为系统阻塞
           case TaskState::sys_block:
               {
                   // 断言任务是否被阻塞
                   assert(tk->block_);
                   if (tk->block_) {
                       // 从链表中移除该任务
                       it = slist.erase(it);
                       // 将任务添加到阻塞任务的等待队列中
                       if (!tk->block_->AddWaitTask(tk))
                           runnable_list_.push(tk);
                   }
               }
               break;

           // 任务状态为完成
           case TaskState::done:
           default:
               // 任务数量减一
               --task_count_;
               // 完成任务数量加一
               ++done_count;
               // 从链表中移除该任务
               it = slist.erase(it);
               DebugPrint(dbg_task, "task(%s) done.", tk->DebugInfo());
               // 如果任务有异常,抛出异常
               if (tk->eptr_) {
                   std::exception_ptr ep = tk->eptr_;
                   runnable_list_.push(slist);
                   tk->DecrementRef();
                   std::rethrow_exception(ep);
               } else
                   // 否则,任务引用减一
                   tk->DecrementRef();
               break;
       }
   }
   // 如果需要重新将任务添加到可运行队列中
   if (do_count)
       runnable_list_.push(slist);

   return c;
}

  • 协程切换 :通过调用协程的SwapOut进行协程切换
void Processer::CoYield(ThreadLocalInfo &info)
{
    // 获取当前线程的任务
    Task *tk = info.current_task;
    // 如果当前线程没有任务,直接返回
    if (!tk) return ;

    // 打印调试信息
    DebugPrint(dbg_yield, "yield task(%s) state=%d", tk->DebugInfo(), tk->state_);
    // 增加任务切换次数
    ++tk->yield_count_;
    // 切换任务
    if (!tk->SwapOut()) {
        // 切换失败,打印错误信息
        fprintf(stderr, "swapcontext error:%s\n", strerror(errno));
        // 抛出异常
        ThrowError(eCoErrorCode::ec_yield_failed);
    }
}

3.协程(task)

协程具有七种状态

enum class TaskState
{
    init,            // 初始化
    runnable,        // 可运行
    io_block,        // 阻塞,包括write, writev, read, select, poll等
    sys_block,       // 阻塞,包括互斥锁等
    sleep,           // 睡眠,包括sleep, nanosleep, poll(NULL, 0, timeout)
    done,     //完成
    fatal,//异常
};

协程结构体

struct Task
    : public TSQueueHook
{
    // 任务ID
    uint64_t id_;
    // 任务状态
    TaskState state_ = TaskState::init;
    // 协程切换次数
    uint64_t yield_count_ = 0;
    // 任务所属进程
    Processer* proc_ = NULL;
    // 上下文
    Context ctx_;
    // 任务函数
    TaskF fn_;
    // 调试信息
    std::string debug_info_;
    // 异常指针
    std::exception_ptr eptr_;           // 保存exception的指针
    // 引用计数
    std::atomic<uint32_t> ref_count_{1};// 引用计数

    // Network IO block所需的数据
    IoWaitData *io_wait_data_ = nullptr;

    // sys_block等待的block对象
    BlockObject* block_ = nullptr;      
    // sys_block等待序号(用于做超时校验)
    uint32_t block_sequence_ = 0;       
    // sys_block超时时间
	MininumTimeDurationType block_timeout_{ 0 }; 
    // sys_block的等待是否超时
    bool is_block_timeout_ = false;     

    // 睡眠时间
    int sleep_ms_ = 0;                  

    // 构造函数
    explicit Task(TaskF const& fn, std::size_t stack_size);
    // 析构函数
    ~Task();

    // 将任务添加到进程
    void AddIntoProcesser(Processer *proc, char* shared_stack, uint32_t shared_stack_cap);

    // 任务调度
    bool SwapIn();
    // 任务暂停
    bool SwapOut();

    void SetDebugInfo(std::string const& info);
    // 获取调试信息
    const char* DebugInfo();

    // 获取IoWaitData
    IoWaitData& GetIoWaitData();

    // 静态变量,用于生成TaskId
    static uint64_t s_id;
    // 静态变量,用于记录Task的引用计数
    static std::atomic<uint64_t> s_task_count;

    // 增加Task的引用计数
    void IncrementRef();
    // 减少Task的引用计数
    void DecrementRef();
    // 获取Task的引用计数
    static uint64_t GetTaskCount();

    // Task引用计数归0时不要立即释放, 以防epoll_wait取到残余数据时访问野指针.
    // 定义一个队列,用于存放Task
    typedef TSQueue<Task> DeleteList;
    // 定义一个智能指针,用于管理DeleteList
    typedef std::shared_ptr<DeleteList> DeleteListPtr;

    // 定义一个LFLock,用于保护s_delete_lists
    static LFLock s_delete_lists_lock;
    // 定义一个列表,用于存放DeleteListPtr
    static std::list<DeleteListPtr> s_delete_lists;

    // 从s_delete_lists中弹出DeleteList,将其中的Task放入output中
    static void PopDeleteList(std::vector<SList<Task>> & output);
    // 获取s_delete_lists中已删除的Task的数量
    static std::size_t GetDeletedTaskCount();
};

协程切出上下文

  • 协程被挂起
    • 系统函数被 hook;
    • libgo_poll (被 hook 的 io 操作函数会调用 libgo_poll 实现切换)
    • select
    • sleep、usleep、nanosleep
    • 调用了协程锁 CoMutex(co_mutex),协程读写锁 CoRWMutex(co_rwmutex),或者使用了 channel
  • 协程执行完毕
  • 通过co_yield主动切出

协程切入上下文

  • 执行器在调度(Process)期间;
  • 唤醒挂起协程不会切入上下文,只是从等待队列中重新添加到 newQueue_。

    4.关系图

在这里插入图片描述

四.上下文切换使方式

libgo中通过libgo/context/context.h定义了切换函数接口,函数实现使用了Boost.Context和ucontext两种实现方式

1.Boost.Context

boost上下文切换

2.ucontext

#include<stdio.h>
#include<ucontext.h>
ucontext_t ctx[2];
ucontext_t main_ctx;
int cont=0;
void func1(void){
   while(cont++<20){
       printf("1\n");
       swapcontext(&ctx[0],&ctx[1]);
       printf("3\n");
   }

}
void func2(void){
   while(cont++<20){
   printf("2\n");
       swapcontext(&ctx[1],&ctx[0]);
       printf("4\n");
   }
}
int main(){
   char stack1[2048]={0};
   char stack2[2048]={0};


   getcontext(&ctx[0]);
   ctx[0].uc_stack.ss_sp=stack1;
   ctx[0].uc_stack.ss_size=sizeof(stack1);
   ctx[0].uc_link=&main_ctx;
   makecontext(&ctx[0],func1,0);

   getcontext(&ctx[1]);
   ctx[1].uc_stack.ss_sp=stack2;
   ctx[1].uc_stack.ss_size=sizeof(stack2);
   ctx[1].uc_link=&main_ctx;
   makecontext(&ctx[1],func2,0);

   printf("swapcontext\n");

   swapcontext(&main_ctx,&ctx[0]);

   printf("end\n");
}

执行结果

swapcontext
1
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
end

3.setjmp

逻辑复杂

#include<stdio.h>
#include<setjmp.h>
jmp_buf env;
void func(int arg){
   printf("func %d\n",arg);
longjmp(env,++arg);
}
int main(){
   int ret =setjmp(env);
   if(ret==0){
       func(ret);
   }else if(ret ==1){
       func(ret);
   }else if(ret ==2){
       func(ret);
   }else if(ret ==3){
       func(ret);
   }
   return 0;
}

执行结果

func 0
func 1
func 2
func 3

4.汇编

基于汇编的 C/C++ 协程 - 切换上下文

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值