是专为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主动切出
协程切入上下文
四.上下文切换使方式
libgo中通过libgo/context/context.h定义了切换函数接口,函数实现使用了Boost.Context和ucontext两种实现方式
1.Boost.Context
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