文章目录
- P26:协程01
- 一、方法函数
- 二、结果展示
- P27-28:协程02-03
- 一、方法函数
- 二、结果展示
- P29:协程04
- 一、方法函数
- 二、结果展示
P26:协程01
本节内容主要介绍了开始协程的一些准备工作,平常我们使用assert
断言时,如果一个函数在很多地方调用并且触发了断言,那么我们就不能通过控制台输出的异常信息知道到底是哪次调用的断言出了问题,debug起来就相当麻烦,sylar
自己设计了一个断言宏函数,能够打印出函数调用栈来解决这个问题。
一、方法函数
backtrace
void Backtrace(std::vector<std::string>& bt, int size, int skip) {
// 没有按照官方库的实现应用指针,这是因为指针会占用栈的空间,一般不应该往栈里面分配大的对象
// 线程的栈一般较大,协程的栈可以我们自己设置,一般较小,便于轻量级切换
void** array = (void**)malloc((sizeof(void*) * size));
size_t s = ::backtrace(array,size);
char** strings = backtrace_symbols(array,s);
if(strings == NULL) {
SYLAR_LOG_ERROR(g_logger) << "backtrace_symbols error";
return;
}
for(size_t i = skip; i < s; ++ i) {
bt.push_back(strings[i]);
}
free(strings);
free(array);
}
std::string BacktraceToString(int size, int skip, const std::string& prefix) {
std::vector<std::string> bt;
Backtrace(bt,size,skip);
std::stringstream ss;
for(size_t i = 0; i < bt.size(); ++ i) {
ss << prefix << bt[i] << std::endl;
}
return ss.str();
}
二、结果展示
P27-28:协程02-03
P27节内容主要开始搭建协程的类,sylar
讲课确实不喜欢解释代码,看完这一节啥都不知道,下列记录了我根据网上他人的一些笔记以及资料对这一节代码的解释,可能理解得不太对,自行参考。
P28节完善了协程类的成员函数以及进行了测试。
一、方法函数
ucontext_t
sylar
的协程模块基ucontext_t
实现,所以先了解一下ucontext_t
的操作函数再去理解代码会容易一些。参考链接
-
ucontext_t的结构体
typedef struct ucontext_t { // 当前上下文结束后,下一个激活的上下文对象的指针,只在当前上下文是由makecontext创建时有效 struct ucontext_t *uc_link; // 当前上下文的信号屏蔽掩码 sigset_t uc_sigmask; // 当前上下文使用的栈内存空间,只在当前上下文是由makecontext创建时有效 stack_t uc_stack; // 平台相关的上下文具体内容,包含寄存器的值 mcontext_t uc_mcontext; ... // 根据平台的不同,结构体成员也有不同,但是上面的4个都是所以平台至少有的 } ucontext_t;
-
ucontext_t
的接口- makecontext(…):该函数会修改由getcontext获取到的上下文指针ucp,将其与一个函数func进行绑定,支持指定func运行时的参数。注意在调用makecontext之前,必须手动给ucp分配一段内存空间,存储在ucp->uc_stack中,这段内存空间将作为func函数运行时的栈空间,同时也可以指定ucp->uc_link,表示函数运行结束后恢复uc_link指向的上下文, 如果不赋值uc_link,那func函数结束时必须调用setcontext或swapcontext以重新指定一个有效的上下文,否则程序就跑飞了。 makecontext执行完后,ucp就与函数func绑定了,调用setcontext或swapcontext激活ucp时,func就会被运行。
- swapcontext:swapcontext是sylar非对称协程实现(子协程只能和线程主协程切换,而不能和另一个子协程切换,并且在程序结束时,一定要再切回主协程,以保证程序能正常结束)的关键,线程主协程和子协程用这个接口进行上下文切换。
// 获取当前的上下文 int getcontext(ucontext_t *ucp); // 恢复ucp指向的上下文,这个函数不会返回,而是会跳转到ucp上下文对应的函数中执行,相当于变相调用了函数 int setcontext(const ucontext_t *ucp); void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); // 恢复ucp指向的上下文,同时将当前的上下文存储到oucp中, // 和setcontext一样,swapcontext也不会返回,而是会跳转到ucp上下文对应的函数中执行,相当于调用了函数 int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
Fiber
下列是协程类目前的代码,因为还没有编译过,所以里面可能存在错误,后面将解释一下这些成员函数和变量的具体含义,以及其它的一些知识点。
class Fiber : public std::enable_shared_from_this<Fiber> {
public:
std::shared_ptr<Fiber> ptr;
enum State {
INIT,
HOLD,
EXEC,
TERM,
READY
};
private:
Fiber();
public:
Fiber(std::function<void()> cb, size_t stacksize = 0);
~Fiber();
// 重置协程函数,并重置状态
void reset(std::function<void()> cb);
// 切换到当前协程执行
void swapIn();
// 切换到后台执行
void swapOut();
public:
// 设置当前协程
static void SetThis(Fiber* f);
// 返回当前协程
static Fiber::ptr GetThis();
// 协程切换到后台,并且设置为Ready状态
static void YieldToReady();
//协程切换到后台,并且设置为Hold状态
static void YieldToHold();
// 总协程数
static uint64_t TotalFibers();
static MainFunc();
private:
uint64_t m_id = 0; //协程号
uint32_t m_stacksize = 0; // 栈的大小
State m_state = INIT; //初试状态
ucontext_t m_ctx; //
void* m_stack = nullptr; //协程栈
std::function<void()> m_cb; // 回调函数
};
enable_shared_from_this
我们第一次使用该类应该是在写日志时,在一个类中需要获取到自己的智能指针,下面详细解释一下该类。参考链接
- 含义:enable_shared_from_this可以让一个对象安全的生成多个实例,并且共享该对象的所有权。如果一个类继承于enable_shared_from_this,并且该类被一个共享指针对象pt管理,那么该类就包含了一个
shared_from_this
的成员函数,调用该函数会返回一个智能指针对象,与pt共享类对象的所有权。 - 使用场合: 当类A被share_ptr管理,且在类A的成员函数里需要把当前类对象作为参数传给其他函数时,就需要传递一个指向自身的share_ptr。
- 使用原因:因为在异步调用中(一个主要使用场景),存在一个保活机制,异步函数执行的时间点我们是无法确定的,然而异步函数可能会使用到异步调用之前就存在的变量。为了保证该变量在异步函数执期间一直有效,我们可以传递一个指向自身的share_ptr给异步函数,这样在异步函数执行期间share_ptr所管理的对象就不会析构,所使用的变量也会一直有效了(保活)。
协程状态
- INIT:初始化状态
- HOLD:暂停状态
- EXEC:执行中状态
- TERM:结束状态
- READY:可执行状态
成员变量
// 协程id
uint64_t m_id = 0;
// 协程运行栈大小
uint32_t m_stacksize = 0;
// 协程状态
State m_state = INIT;
// 上下文
ucontext_t m_ctx;
// 协程运行栈指针
void* m_stack = nullptr;
// 协程执行方法
std::function<void()> m_cb;
全局变量
static std::atomic<uint64_t> s_fiber_id {0}; // 生成协程ID
static std::atomic<uint64_t> s_fiber_count {0}; // 统计当前的协程数
static thread_local Fiber* t_fiber = nullptr; // 当前协程
static thread_local Fiber::ptr t_threadFiber = nullptr; // 主协程
// 设置协程栈的大小为1MB
static ConfigVar<uint32_t>::ptr g_fiber_stack_size =
Config::Lookup<uint32_t>("fiber.stack_size", 1024 * 1024, "fiber stack size");
创建/释放运行栈
class MallocStackAllocator {
public:
static void* Alloc(size_t size) {
return malloc(size);
}
static void Dealloc(void* vp, size_t size) {
return free(vp);
}
};
成员函数
-
无参构造函数
// 主协程的构造 Fiber::Fiber() { m_state = EXEC; // 设置当前协程 SetThis(this); // 获取当前协程的上下文信息保存到m_ctx中 if(getcontext(&m_ctx)) { SYLAR_ASSERT2(false,"getcontext"); } ++ s_fiber_count; }
-
有参构造函数
Fiber::Fiber(std::function<void()> cb, size_t stacksize) : 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); // 保存当前协程上下文信息到m_ctx中 if(getcontext(&m_ctx)) { SYLAR_ASSERT2(false,"getcontext"); } // uc_link为空,执行完当前context之后退出程序。 m_ctx.uc_link= nullptr; // 初始化栈指针 m_ctx.uc_stack.ss_sp = m_stack; // 初始化栈大小 m_ctx.uc_stack.ss_size = m_stacksize; makecontext(&m_ctx, &Fiber::MainFunc, 0); }
-
析构函数
// 释放协程运行栈 Fiber::~Fiber() { --s_fiber_count; // 子协程 if(m_stack) { // 不在准备和运行状态 SYLAR_ASSERT(m_state == TERM || m_state == INIT); // 释放运行栈 StackAllocator::Dealloc(m_stack, m_stacksize); } else { // 主协程的释放要保证没有任务并且当前正在运行 SYLAR_ASSERT(!m_cb); SYLAR_ASSERT(m_state == EXEC); // 若当前协程为主协程,将当前协程置为空 Fiber* cur = t_fiber; if(cur == this) { SetThis(nullptr); } } }
-
重置协程
void Fiber::reset(std::function<void()> cb) { // 要求栈空间 SYLAR_ASSERT(m_stack); // 要求状态只能为结束或者初始状态 SYLAR_ASSERT(m_state == TERM || m_state == INIT || m_state == EXCEPT); m_cb = cb; 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; makecontext(&m_ctx, &Fiber::MainFunc, 0); m_state = INIT; }
-
设置当前协程
void Fiber::SetThis(Fiber* f) { t_fiber = f; }
-
切换协程
void Fiber::swapIn() { SetThis(this); SYLAR_ASSERT(m_state != EXEC); // 因为要执行切换,所以改为运行状态 m_state =EXEC; if(swapcontext(&t_threadFiber->m_ctx, &m_ctx)) { SYLAR_ASSERT2(false, "swapcontext"); } } void Fiber::swapOut() { SetThis(t_threadFiber.get()); if(swapcontext(&m_ctx, &t_threadFiber->m_ctx)) { SYLAR_ASSERT2(false, "swapcontext"); } }
-
获取当前协程
Fiber::ptr Fiber::GetThis() { // 返回当前协程 if(t_fiber) { return t_fiber->shared_from_this(); } // 获得主协程 Fiber::ptr main_fiber(new Fiber); // 确保当前协程为主协程 SYLAR_ASSERT(t_fiber == main_fiber.get()); t_threadFiber = main_fiber; return t_fiber->shared_from_this(); }
-
切换协程到后台,并且赋予状态
void Fiber::YieldToReady() { Fiber::ptr cur = GetThis(); cur->m_state = READY; cur->swapOut(); } void Fiber::YieldToHold() { Fiber::ptr cur = GetThis(); cur->m_state = HOLD; cur->swapOut(); }
-
总协程数和获取协程id
uint64_t TotalFibers() { return s_fiber_count; } uint64_t Fiber::GetFiberId() { if(t_fiber) { return t_fiber->getId(); } return 0; }
-
主函数
void 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(); } catch(...) { cur->m_state = EXCEPT; SYLAR_LOG_ERROR(g_logger) << "Fiber Except: "; } }
二、结果展示
测试代码
其它配置需要跟着视频更改
#include "../sylar/sylar.h"
sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();
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();
}
int main(int argc, char** argv) {
sylar::Fiber::GetThis();
SYLAR_LOG_INFO(g_logger) << "main begin";
sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber));
fiber->swapIn();
SYLAR_LOG_INFO(g_logger) << "main after swapIn";
fiber->swapIn();
SYLAR_LOG_INFO(g_logger) << "main after end";
return 0;
}
结果
目前存在问题,析构函数只执行了一次。
P29:协程04
本节内容主要有以下:
- 解决子协程运行完毕后无法回到主协程问题
- 新增log打印出线程名称
- 运行一个多线程多协程的例子
一、方法函数
对于下列测试用例, SYLAR_LOG_INFO(g_logger) << "main after end2";
是无法被执行的,因为我们之前的子协程在运行完后没有做任何的处理,导致无法回归到主协程
SYLAR_LOG_INFO(g_logger) << "main begin -1";
{
sylar::Fiber::GetThis();
SYLAR_LOG_INFO(g_logger) << "main begin";
sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber));
fiber->swapIn();
SYLAR_LOG_INFO(g_logger) << "main after swapIn";
fiber->swapIn();
SYLAR_LOG_INFO(g_logger) << "main after end";
fiber->swapIn();
}
SYLAR_LOG_INFO(g_logger) << "main after end2";
改进:在协程运行主函数中结果后通过swapOut
手动切换为主线程,此外这里还手动对子协程引用的智能指针对象进行了释放,防止析构函数调用出问题。
void 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();
} catch(...) {
cur->m_state = EXCEPT;
SYLAR_LOG_ERROR(g_logger) << "Fiber Except: ";
}
// 这里cur获得一个智能指针导致计数器加一,但是由于最后没有释放,它会一直在栈上面,所以
// 该对象的智能指针计数永远大于等于1,无法被释放
auto raw_ptr = cur.get();
cur.reset();
// 返回主协程
raw_ptr->swapOut();
SYLAR_ASSERT2(false, "never reach");
}
二、结果展示
多线程多协程例子
#include "../sylar/sylar.h"
sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();
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();
}
void test_fiber() {
SYLAR_LOG_INFO(g_logger) << "main begin -1";
{
sylar::Fiber::GetThis();
SYLAR_LOG_INFO(g_logger) << "main begin";
sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber));
fiber->swapIn();
SYLAR_LOG_INFO(g_logger) << "main after swapIn";
fiber->swapIn();
SYLAR_LOG_INFO(g_logger) << "main after end";
fiber->swapIn();
}
SYLAR_LOG_INFO(g_logger) << "main after end2";
}
int main(int argc, char** argv) {
sylar::Thread::SetName("main");
std::vector<sylar::Thread::ptr> thrs;
for(size_t i = 0; i < 3; ++ i) {
thrs.push_back(sylar::Thread::ptr(new sylar::Thread(&test_fiber, "name_" + std::to_string(i))));
}
for(auto i : thrs) {
i->join();
}
return 0;
}
生成了3个线程,每个线程含有一个主协程两个子协程