一 什么是协程
协程现在已经不是一个新的技术了,但是由于之前一直在用较低版本的c++,没什么机会使用协程。最近写了不少go的代码,接触到了协程,所以想从零开始学习一下协程。
1. 到底什么是协程
之前听说协程的时候,大家都讲协程就是执行在用户态的微线程,加上go中协程的使用和线程差不多,我也就一直这样理解了。但是真正定义协程的功能是:可以随时的挂起和恢复,它允许多个入口点在不同的执行点挂起和恢复,而不是像线程那样在任何时候都可能被系统强制切换。那么可以随时挂起和恢复到底能解决什么问题呢?下面我们来谈谈协程的优势。
2. 协程的优势
协程拥有轻量,高效,简单等优势。
- 轻量:协程一般都是在各个语言的层面上做实现,线程仍然是操作系统运算调度的最小单位,比起线程来,创建协程更加轻量。协程有多种实现方式,当我们在一个线程上分配多个协程时,协程之间就不需要考虑锁机制。
- 高效:当我们的线程在执行IO密集型操作时,往往需要等待IO结果,此时操作系统要么做线程的切换,而频繁的切换线程是一个和高额的操作,当使用协程的时候,我们在线程内使用协程将操作挂起,等待IO完成时再继续执行,这样不会发生线程切换等操作。
- 简化异步编程:在我们使用rpc框架时,框架往往会提供同步,异步等调用方式,当同步调用其他接口时,当前线程会被阻塞,当异步调用其他接口时,就需要你提供一个回调函数,当有结果返回时,由框架将结果回吐给你。这种编程方式是不方便的,协程可以简化这个操作,后面我们会举例说明。
下面我会介绍协程是如何产生上述优势的,行文逻辑如下,在第二个章节,我会介绍,当已知了协程的功能,使用协程的时候,我们如何简化了异步编程;第三个章节我们会介绍协程是如何实现我们希望的那些功能的。
二 使用协程异步编程
使用异步做网络编程(实现业务逻辑)时,我们的业务代码是有严格的执行顺序的,但是异步的返回是无序的,就使得我们,代码往往需要一些状态码来判断前置调用是否已经完成,如果再叠加了异常处理这些逻辑的话,代码逻辑会非常晦涩难懂,而且容易经常性的形成回调地狱。举个例子,如果我们使用异步回调的方式对一个整型数字做加3的操作,我们有一个加1的函数,加3时需要调用三次:
void AsyncAddOne(int val, std::function callback) {
std::thread t([value, callback = std::move(callback)] {
callback(val + 1);
});
t.detach();
}
AsyncAddOne(1, [] (int result) {
AsyncAddOne(result, [] (int result) {
AsyncAddOne(result, [] (int result) {
cout << "result is: " << result << endl;
});
});
});
看起来十分的晦涩难懂,现在大部门的服务框架其实已经做了一些优化,比如使用Promise/Future特性。下面只是简单示意一下:
AddOne.then({return AddOne.then({return AddOnde})})
我们拿一个在日常生产过程中的一段实例来示范Promise/Future特性,
示例如下:这段代码的逻辑是使用了两个异步线程分别调用了redis和mysql,拿到结果后做自身的业务处理请求
// 第一个串行任务,CommonTask
trpc::Future CommonHandler() {
// 1. do something in common handler
return MakeReadyFuture(res);
}
void HttpHandler() {
// 1. 处理公共逻辑
auto http_task = CommonHandler();
// 2. 任务1完成后,创建并执行并行任务
auto data_task = http_task.Then([](Future&& result1) {
// 2.1 创建redis任务,通过redis_proxy发起调用, 并返回相关结果,cmd为请求redis的命令
trpc::Future fut_redis_task = redis_proxy->AsyncRedis(cmd);
// 2.2 创建mysql任务, 通过mysql_proxy发起调用, 并返回相关结果,cmd为请求mysql的命令
trpc::Future fut_mysql_task = mysql_proxy->AsyncMysql(cmd);
// 将单个任务加入parallel_futs
parallel_futs.push_back(fut_redis_task);
parallel_futs.push_back(fut_mysql_task);
// 若并行任务2.1和2.2都完成了则结束该回调,并进入下一个回调
auto fut = WhenAll(parallel_futs).Then([](std::vector>&& result2) {
// 分别获得redis和mysql的result, 进而完成相关任务
// result[0].GetValue();
// result[1].GetValue();
// 3. do something calc handler...
return trpc::MakeReadyFuture(res);
});
return fut;
});
// 回包
data_task.Then([](Future&& result3){
if (result3.IsReady()) {
// 4. do something and response to client
// full succ in reply
} else {
// full exception in reply
}
SendUnaryResponse(reply);
// 链式调用最后的then可以返回void
});
}
虽然Future这种模式已经简化了之前自己写代码判断各个异步任务的完成状态(实际上是封装在了Future自身的逻辑中),但是也有一定的编程复杂度,尤其在涉及到错误处理的时候。
使用协程可以让我们像使用一个线程做同步调用一样,来写我们的一部调用代码。具体是如何做到的,可以参照下文的实现。
三 协程是如何实现的
协程的实现方式有很多种,具体到线程这个点上,有M:N和1:N的实现方式,M:N就是在M个线程上启用N个协程,1:N就是在1个线程上开启N个协程,这两种实现区别也是显而易见的,M:N可以充分利用cpu性能,1:N实现不需要考虑协程间的竞争问题。
我们回顾一下协程需要实现的功能:
- 任务挂起
- 任务恢复
所以在实现协程时,挂起(co_yield)需要保存当前函数执行的上下文,在恢复执行(co_resume)时需要恢复函数栈帧重新执行。做此类实现一般都需要借助汇编,这里列举几个协程库:https://github.com/Tencent/libco
https://github.com/boostorg/fiber
微信的libco同时也hook了recv等系统调用,在执行网络IO时会自行让渡,在使用时需要加上特殊的链接参数。
后面会对libco做一些分析(未完待续)