什么是协程?
协程:协同运行的子程序(函数);
协程:协同运行的进程(shell中的概念)
协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且从栈分离存储恢复所要求的数据。这允许编写异步执行的顺序代码(例如不使用显式的回调来处理非阻塞 I/O),还支持对惰性计算的无限序列上的算法及其他用途。
协程可以被视为提供特殊控制流的语言级构造。
与抢占式线程相比,协程开关是协作的(程序员控制开关何时发生)。协程开关中不涉及内核。
单核情况下,线程和协程都是不能并行的,但是谁快?是协程更快,因为协程有更少的上下文切换,而且, 同一个线程内的不同协程之间的切换由用户态完成,而线程的切换需要内核态的cpu去保存上下文环境,所以协程更快。
多核的情况下,一个线程只能被一个cpu执行,那么当前,一个线程里的多个协程只能是并发的,不能是并行的,而多个线程下的多个协程就可以是并行的,并且速度也是更快。
协程可以干什么?
当一个函数阻塞线程的时候,我们为了将同步改为异步,我们一般会选择多线程或者使用回调机制来将同步改成异步,用的最多的就是IO多路复用,但是,协程,既没有用线程,也没有用IO多路复用技术,就能实现不同函数之间的异步执行,从来提升了效率,在不使用线程,即没有上下文切换的负担下完成了高并发。
比如,当做IO的时候,我们可以把当前的线程thread_yield出让调度器,去执行别的线程,等再次调度到这个线程的时候再继续执行,这样会有一次上下文切换,比较浪费时间,所以诞生了IO多路复用,它类似Qt的信号槽机制,类似信号,IO进行的时候,我就去做别的事情,等io做完了,给我一个信号,或者给我一个信息,我执行相应的函数。
C++20已经提出了协程的概念,并完成了底层函数,但是没有提供标准库。
Libco为腾讯开源的C++常用的协程库。
libco通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法,如线程库一样轻松。同时库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。、
我一直感觉协程和IO多路复用技术很像,有点分不清,网传了一篇文章,说实际在业务上的IO多路复用,常常因为需要保存事件前后的状态而变得复杂,而协程恰恰可以自动保存这些状态,所以说IO多路复用,如果能和协程结合起来,会有奇效,不过这估计也只有到了实际业务,有大佬带着才好写了,没有经验的估计搞不出来。
libco的特性
- 无需侵入业务逻辑,把多进程、多线程服务改造成协程服务,并发能力得到百倍提升;
- 支持CGI框架,轻松构建web服务(New);
- 支持gethostbyname、mysqlclient、ssl等常用第三库(New);
- 可选的共享栈模式,单机轻松接入千万连接(New);
- 完善简洁的协程编程接口 类pthread接口设计,通过co_create、co_resume等简单清晰接口即可完成协程的创建与恢复;
- __thread的协程私有变量、协程间通信的协程信号量co_signal (New);
- 语言级别的lambda实现,结合协程原地编写并执行后台异步任务 (New);
- 基于epoll/kqueue实现的小而轻的网络框架,基于时间轮盘实现的高性能定时器;
libco的安装
libco源码可以在腾讯开源里面找到,连接的GitHub上也有安装步骤,文件很少,直接make就可以,或者用cmake构建。
之后把.h文件cp到 /usr/include文件里,把solib文件夹里的动态库文件cp到/usr/lib64/下。
生产者消费者例子
// 使用生产者消费者问题学习协程
#include <co_routine.h>
#include <stdlib.h>
#include <time.h>
#include <vector>
#include <string>
#include <iostream>
// 定义消息内存
struct stPara_t
{
// 条件变量
stCoCond_t *cond;
// 数据池
std::vector<int> vecData;
// 数据ID
int id;
// 协程ID,每个协程保存副本?
int cid;
};
// 生产者函数
void* Producer(void *arg);
// 消费者函数
void* Consumer(void *arg);
int main()
{
// 定义数据池
stPara_t p;
p.cond = co_cond_alloc();
p.cid = 0;
p.id = 0;
// 设置随机数的种子,使用time提供的时间来初始化种子,保证能得到比较随机的种子
srand(time(NULL));
// 创建协程对象,一个生产者,两个消费者
stCoRoutine_t *pProducerCo = nullptr;
stCoRoutine_t *pConsumerCo[2] = {nullptr};
// 创建生产者协程
// 调用函数创建协程对象,函数内分配对象指针
// 此函数不会报错
/**
* 1.出参,返回创建的协程对象
* 2.入参,指定创建协程的属性,默认属性为空
* 3.入参,指定协程执行的函数
* 4.入参,执行函数的参数
*/
co_create(&pProducerCo,NULL,Producer,&p);
// 启动协程
co_resume(pProducerCo);
std::cout << "start producer coroutine success" << std::endl;
// 创建消费者协程
for (int i = 0; i < 2; i++)
{
co_create(&pConsumerCo[i],NULL,Consumer,&p);
co_resume(pConsumerCo[i]);
}
std::cout << "start consumer coroutine success" << std::endl;
// 启动循环事件,执行epoll的事件循环处理,那么不就是底层调用了epoll么?
co_eventloop(co_get_epoll_ct(),NULL,NULL);
exit(0);
}
void* Producer(void *arg)
{
// 启用协程hook(钩子)
co_enable_hook_sys();
stPara_t *p = (stPara_t *)arg;
int cid = ++p->cid;
while (true)
{
// 产生随机个数据
for (int i = rand()%5 + 1; i > 0; --i)
{
p->vecData.push_back(++p->id);
std::cout << cid << " cid add new data :" << p->id << std::endl;
}
// 通知消费者
co_cond_signal(p->cond);
// 必须使用poll等待,不然没法切换协程
// 第三个参数是等待的时间,毫秒,哪怕是1ms,消费者协程都可以执行完毕
poll(NULL,0,1);
}
return NULL;
}
void* Consumer(void *arg)
{
// 启动协程hook
co_enable_hook_sys();
stPara_t *p = (stPara_t *)arg;
int cid = ++p->cid;
while (true)
{
// 检查数据池,无数据则等待通知
if(p->vecData.empty())
{
co_cond_timedwait(p->cond,-1);
continue;
}
// 消费数据
std::cout << cid << " cid del data:" << p->vecData.front() << std::endl;
p->vecData.erase(p->vecData.begin());
}
return NULL;
}
makefile
CFLAGS+=-g -Wall
LDFLAGS+= -L/home/adce/libco/libco-master/lib # 静态库.a文件的路径
LDLIBS+= -lstdc++ -lcolib -pthread -ldl
all:libco1
libco1:libco1.o
$(CC) $^ -o $@ $(CFLAGS) $(LDFLAGS) $(LDLIBS)
clean:
rm -rf *.o libco1