协程是什么
从进程和线程开始
当一个程序启动运行之后在操作系统中就成为了一个进程,操作系统会维护一个PCB数据结构作为进程的控制块,然后操作系统就会调用复杂的调度系统将进程分配给CPU核心进行执行
而由于各种并发需求的出现,人们又从进程中抽象出了一个线程的概念,线程其实就是一个执行代码逻辑流,现代CPU上其实跑的是线程执行流
线程的出现大大提高了CPU的利用率,当一个线程执行流因为IO等事件阻塞等待时,操作系统可以进行更细粒度的切换来合理利用CPU,提高了整体的性能
总体看起来就像下图(操作系统中的线程一般会通过一些技术映射控制用户线程)
当前存在的问题
线程提出其实已经足以解决很多问题,但是由于后续的各种业务的需求花样越来越多,业务的数据量也疯狂增长,在客户端和服务端的架构设计和编码上出现了一些问题:如何解决整体回调过多、如何在线程数量一定的情况下应付高并发场景
所以就在线程的基础上建立了协程的概念,这个协程从架构上看其实就是一个个任务的抽象,一个线程绑定一个任务队列,然后不断处理这些任务,一旦遇到阻塞的任务就通过一些手段将其挂起,然后切换其他任务继续运行(这里存在多种实现方式,只要不卡死在某个任务就行)
对于一些客户端的场景,个人比较了解的就是像Kotlin的挂起和唤醒编程,react后续的时间片优化等
对于服务端场景,例如go语言自带的协程调度系统、腾讯开源的libco框架等
关于协程的实现
线程运行协程和线程运行一个普通函数的根本区别就在于能不能被挂起和唤醒(类似goto)
所以要实现协程一个重要的点就是如何实现上下文切换,把线程的上下文切换放到用户态来做,就可以减少用户态转成内核态的各种开销,增大的系统的吞吐量。而要模拟切换线程上下文,其实是设置寄存器的值,还有执行的函数入口等(在linux中我们可以使用ucontext这个东西进行切换,非常方便),这里如果手动实现需要编写汇编代码,在程序运行时进行动态切换上下文
我自己实现了一个手动协程切换的库(部分还在编写中,不过切换功能已实现,供大家可以参考基本原理):
https://github.com/Prince-Hervoet/RelaxingRoutines_auto
封装一个协程结构:
class RoutineProcess
{
public:
unsigned long long id;
// the task func of this routine
TaskFunc task;
void *args;
// the stack saved when the current coroutine was swapped out
char *save;
Controller *con;
ucontext_t current;
int status = INIT;
int saveSize = 0;
int capSize = 0;
public:
RoutineProcess(unsigned long long id, TaskFunc task, void *args)
{
this->id = id;
this->task = task;
this->args = args;
}
~RoutineProcess()
{
if (save)
{
delete[] save;
}
}
};
封装一个线程控制器:
class Controller
{
friend void threadFunc(void *args);
private:
// running stack: it is opened in the thread
char runningStack[RUNNING_SIZE];
// routines,why set?
/*
We're not sure if we're going to use id maps or something else,
so we're going to use a collection, so we can just grab it.
*/
std::set<RoutineHandler *, HandlerComparator> routineHandlers;
unsigned long long increment;
RoutineHandler *running;
// epoll event list
EpollPack *ep;
// timer task list
DelayQueue<RoutineEvent> *dq;
// this context of the main routine
ucontext_t host;
int size = 0;
int limit = 0;
public:
Controller() {}
Controller(int limit);
~Controller()
{
if (running)
{
delete running;
}
if (ep)
{
delete ep;
}
if (dq)
{
delete dq;
}
for (auto it = routineHandlers.begin(); it != routineHandlers.end(); ++it)
{
RoutineHandler *rh = *it;
delete rh;
}
routineHandlers.clear();
}
RoutineHandler *createRoutine(TaskFunc task, void *args);
void pendRoutine();
void resumeRouine(RoutineHandler *rh);
void removeRoutine(RoutineHandler *rh);
void addEpollEvent(int sockfd, int eventType);
void removeEpollEvent(int sockfd);
void addTimedTask(int sockfd, long long will);
RoutineHandler *getRunning()
{
return running;
}
int getSize()
{
return size;
}
};
这里由于是手动库,我自己使用thread_local这个线程私有属性(JAVA的同学应该比较熟悉),只要在当前线程使用了controller,就会绑定在当前线程的这个变量上
然后这个库实现采用的是共享栈协程,就是当每个协程开始运行的时候,都是在同一个栈空间上运行,如果前一个协程没有运行完,则会被换出并保存到另一个空间中
上面的flag变量是用于后面的空间计算,因为flag变量分配的时候在最上面
其他具体的代码可以看库的实现hhh,实现起来的效果就是:
#include "simple_routine.hpp"
#include <iostream>
void *test(void *args)
{
std::cout << "one" << std::endl;
simple_await();
std::cout << "three" << std::endl;
return nullptr;
}
int main()
{
RoutineHandler *rh = simple_new(test, nullptr);
simple_resume(rh);
std::cout << "two" << std::endl;
simple_resume(rh);
return 0;
}
// log: one
two
one
three
最后,感谢大家的阅读!欢迎有兴趣者一起讨论学习!