这个作业属于哪个课程 | 广工2023软件工程课程 |
---|---|
这个作业要求在哪里 | 作业要求 |
文章内容 | 第四篇敏捷冲刺 |
文章目录
一、任务分配与预期任务量
模块 | 主要负责人 |
---|---|
日志模块 | 钟海超 |
配置模块 | 李昊旃 |
线程模块 | 江周勉 |
协程模块 | 宫旭 |
协程调度模块 | 赵光明 |
I/O协程调度模块 | 李伟东 |
Hook模块 | 邱棋浩(组长) |
二、近日任务安排
时间 | 任务 |
---|---|
今天 | 协程模块 |
明天 | 协程调度模块 |
三、实现
1.为什么需要协程模块
1.1简介
使用协程模块可以使得异步编程更加简单和高效。协程可以将异步I/O操作转换为同步的操作,从而让代码更加易读和易于维护。同时,协程模块可以提高代码的性能,因为协程在切换时不需要进行上下文的保存和恢复,避免了线程切换的开销和竞争。
另外,在分布式架构中,协程模块也可以用于实现协程调度和协程间通信。例如,可以将协程模块和消息队列结合使用,实现多个协程之间的消息传递和调度,从而达到分布式协作的目的。
总之,协程模块是C++服务端高并发分布式架构中不可或缺的组件之一,它可以大大提高系统的性能和并发能力,同时也使得代码更加简单、高效和易于维护。
1.2介绍
每个协程在创建时都会指定一个入口函数,类似线程。协程的本质就是函数和函数运行状态的组合。
在普通函数中,函数一旦被调用,只能从头开始执行,直到函数执行结束退出;协程可以执行一半就退出(call),但并未真正结束,只是暂时让出CPU执行权,之后可以恢复运行(back)。在暂停运行期间,其他协程可以获得CPU并运行,因此协程也称为轻量级线程。
2.整体设计
2.1主要功能
- 使用非对称协程模型,简化程序逻辑
- 由用户控制协程的执行逻辑,实现了主协程与子协程间的自由切换
- 每个线程有一个主协程t_threadFiber,由主协程创建子协程,通过call()进入子协程运行,back()退出子协程,返回主协程
2.2功能演示
Logger::ptr logger = LOG_ROOT();
void run_in_fiber() {
LOG_INFO(logger) << "run in fiber";
}
int main() {
LOG_INFO(logger) << "main begin";
Fiber::Getthis();
Fiber::ptr fiber(new Fiber(run_fiber))
fiber.call();
LOG_INFO(logger) << "main end";
return 0;
}
3模块介绍
3.1Fiber
- 协程模块,根据协程状态,实现主协程和子协程间的相互切换
- 协程状态
enum State
{
INIT , // 初始状态
READY, // 可执行状态
RUNNING, // 运行状态
TERM, // 结束状态
};
- 协程主要功能
class Fiber : public std::enable_shared_from_this<Fiber>
{
public:
typedef std::shared_ptr<Fiber> ptr;
public:
/// <summary>
/// 无参构造函数
/// 每个线程第一个协程的构造,主协程
/// </summary>
Fiber();
public:
/// <summary>
/// 构造函数
/// </summary>
/// <param name="cb">协程执行函数</param>
/// <param name="stacksize">协程栈大小</param>
/// <param name="use_caller">Schedule中是否使用主线程</param>
Fiber(std::function<void()> cb, size_t stacksize = 0, bool use_caller = false);
~Fiber();
//重置协程函数,并重置状态
//pre: getState() 为 INIT, TERM, EXCEPT
//post: INIT
void reset(std::function<void()> cb);
// 将当前协程切换到运行状态, Scheduler调度线程 --> 当前线程
void swapIn();
// 退出当前协程,当前协程 --> Scheduler调度协程
void swapOut();
// 将当前协程切换到执行状态 主协程-->当前协程
void call();
// 将当前协程切换到后台,当前协程-->主协程
void back();
public:
/// <summary>
/// 设置当前线程的运行协程
/// </summary>
/// <param name="fiber">运行协程</param>
static void SetThis(Fiber* fiber);
/// <summary>
/// 返回当前协程
/// </summary>
/// <returns>一个全局静态变量 static thread_globle Thread* t_fiber</returns>
static Fiber::ptr GetThis();
// 协程切换到后台,并设置为READY状态。回到Scheduler协程
static void YieldToReadyBySwap();
// 协程切换到后台,并设置为HOLD状态。回到Scheduler协程
static void YieldToHoldBySwap();
// 协程切换到后台,并设置为Ready状态。回到主协程
static void YieldToReadyByBack();
// 协程切换到后台,并设置为HOLD状态。回到主协程
static void YieldToHoldByBack();
/// <summary>
/// 协程的工作函数,执行完成返回到Scheduler协程
/// </summary>
static void MainFunc();
/**
* @brief 协程执行函数
* @post use_caller时有效,Scheduler会使用主线程的时候
*/
static void CallerMainFunc();
private:
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::Fiber() {
m_state = EXEC; //主协程创建好后,是运行中状态
SetThis(this); //设置当前协程 t_fiber
if (getcontext(&m_ctx)) { DO_ASSERT2(false, "getcontext"); }
++s_fiber_count;
m_id = ++s_fiber_id;
LOG_DEBUG(g_logger) << "Fiber::Fiber main id=" + std::to_string(GetId());
}
- 子协程。需要设置协程执行函数
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)) { DO_ASSERT2(false, "getcontext"); }
m_ctx.uc_link = nullptr;
m_ctx.uc_stack.ss_sp = m_stack;
m_ctx.uc_stack.ss_size = m_stacksize;
// Scheduler调度协程中使用主线程参与调度
if (use_caller)
{
makecontext(&m_ctx, &Fiber::CallerMainFunc, 0);
}
else
{
makecontext(&m_ctx, &Fiber::MainFunc, 0);
}
}
- 主协程和子协程间的相互切换
// 主协程-->子协程
void Fiber::call() {
// 设置当前执行的协程:主协程-->字协程
SetThis(this);
m_state = EXEC;
if (swapcontext(&t_threadFiber->m_ctx, &m_ctx)) { DO_ASSERT2(false, "swapcontext"); }
}
// 子协程-->主协程
void Fiber::back() {
// 设置当前执行的协程:子协程-->主协程
SetThis(t_threadFiber.get());
if (swapcontext(&m_ctx, &t_threadFiber->m_ctx)) { DO_ASSERT2(false, "swapcontext"); }
}
//协程切换到后台,并设置为READY状态
//回到主协程
void Fiber::YieldToReadyByBack() {
Fiber::ptr cur = GetThis();
cur->m_state = READY;
cur->back();
}
//协程切换到后台,并设置为HOLD状态
//回到主协程
void Fiber::YieldToHoldByBack() {
Fiber::ptr cur = GetThis();
cur->m_state = HOLD;
cur->back();
}
- 配合Schedule调度协程模块,为了提高效率,通过use_caller控制是否把主线程加入到调度协程任务上,因此涉及到调度协程和子协程间的切换,主协程和子协程间的切换
Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool use_caller)
{
//....
// Scheduler调度协程中使用主线程参与调度
if (use_caller)
{
// 在主协程和主协程间切换
makecontext(&m_ctx, &Fiber::CallerMainFunc, 0);
}
else
{
// 在调度协程和子协程间切换
makecontext(&m_ctx, &Fiber::MainFunc, 0);
}
}
void Fiber::swapIn() // 调度协程-->当前协程
void Fiber::swapOut() // 当前协程-->调度协程
- 协程入口函数
/**
* @brief 协程入口函数
* @note 这里没有处理协程函数出现异常的情况,同样是为了简化状态管理,并且个人认为协程的异常不应该由框架处理,应该由开发者自行处理
*/
void Fiber::CallerMainFunc() {
Fiber::ptr cur = GetThis(); // GetThis()的shared_from_this()方法让引用计数加1
SYLAR_ASSERT(cur);
cur->m_cb(); // 这里真正执行协程的入口函数
cur->m_cb = nullptr;
cur->m_state = TERM;
auto raw_ptr = cur.get(); // 手动让t_fiber的引用计数减1
cur.reset();
raw_ptr->back(); // 协程结束时自动back,以回到主协程
}
- 协程重置。重置协程就是重复利用已结束的协程,复用其栈空间,创建新协程,实现如下
// 只有TERM状态的协程才可以重置
void Fiber::reset(std::function<void()> cb) {
DO_ASSERT(m_stack);
DO_ASSERT(m_state == TERM || m_state == INIT);
m_cb = cb;
if (getcontext(&m_ctx)) {
DO_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 = READY;
}
3.2线程局部变量实现协程模块
- 线程局部变量与全局变量类似,不同之处在于,线程局部变量的每个线程都独有一份;全部变量是全部线程共享一份。static thread_local
- 使用线程局部变量保存协程上下文,每个线程都要独自管理协程,不同线程的协程相互不影响
//当前运行协程
static thread_local Fiber* t_fiber = nullptr;
//主协程
static thread_local Fiber::ptr t_threadFiber = nullptr;
t_fiber是当前运行的协程,t_threadFiber是主协程,通过setThis()设置当前运行的协程,getThis()获取当前的协程
// 没有当前协程时,会创建一个主协程
Fiber::ptr Fiber::GetThis()
void Fiber::SetThis(Fiber* fiber)
使用swapcontext来做协程切换,意味着,这两个线程局部变量必须至少有一个是用来保存线程主协程的上下文,如果这两个线程局部变量存储的都是子协程的上下文,那么不管怎么调用swapcontext,都没法恢复主协程的上下文,也就意味着程序最终无法回到主协程去执行,程序也就跑飞了。
// 当前协程-->主协程
void Fiber::back()
{
//当前协程-->主协程
SetThis(t_threadFiber.get());
if (swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
DO_ASSERT2(false, "swapcontext");
}
}
3.3注意
- 子协程不能直接call另一个子协程,像下面这样的代码会直接让程序跑飞:
Logger::ptr g_logger = LOG_ROOT();
void run_in_fiber2() {
LOG_INFO(g_logger) << "run_in_fiber2 begin";
LOG_INFO(g_logger) << "run_in_fiber2 end";
}
void run_in_fiber() {
LOG_INFO(g_logger) << "run_in_fiber begin";
/**
* 非对称协程,子协程不能创建并运行新的子协程,下面的操作是有问题的,
* 子协程再创建子协程,原来的主协程就跑飞了
*/
Fiber::ptr fiber(new Fiber(run_in_fiber2));
fiber->call();
LOG_INFO(g_logger) << "run_in_fiber end";
}
int main(int argc, char *argv[]) {
LOG_INFO(g_logger) << "main begin";
// 创建主协程,t_threadFiber被设置为当前协程,t_fiber也设置为当前协程
Fiber::GetThis();
// 创建子协程
Fiber::ptr fiber(new Fiber(run_in_fiber));
// 运行子协程,t_fiber设置为子协程
fiber->call();
LOG_INFO(g_logger) << "main end";
return 0;
}
究其原因,在于上面的run_in_fiber本身是一个子协程,在其内部执行另一个协程的call时,swapcontext会把run_in_fiber的上下文保存到t_threadFiber中,导致t_threadFiber不再指向main函数的上下文,导致程序跑飞。
四、感想与期望
今天是敏捷冲刺的第四天,内容是实现和测试协程模块。团队一起交流编程遇到的问题相信,在未来,也希望协程模块得到更广泛的应用和发展。