一 什么是协程
协程当初曾经不是一个新的技术了,然而因为之前始终在用较低版本的 c ++,没什么机会应用协程。最近写了不少 go 的代码,接触到了协程,所以想从零开始学习一下协程。
1. 到底什么是协程
之前据说协程的时候,大家都讲协程就是执行在用户态的微线程,加上 go 中协程的应用和线程差不多,我也就始终这样了解了。然而真正定义协程的性能是:能够随时的挂起和复原,它容许多个入口点在不同的执行点挂起和复原,而不是像线程那样在任何时候都可能被零碎强制切换。那么能够随时挂起和复原到底能解决什么问题呢?上面咱们来谈谈协程的劣势。
2. 协程的劣势
协程领有轻量,高效,简略等劣势。
- 轻量:协程个别都是在各个语言的层面上做实现,线程依然是操作系统运算调度的最小单位,比起线程来,创立协程更加轻量。协程有多种实现形式,当咱们在一个线程上调配多个协程时,协程之间就不须要思考锁机制。
- 高效:当咱们的线程在执行 IO 密集型操作时,往往须要期待 IO 后果,此时操作系统要么做线程的切换,而频繁的切换线程是一个和高额的操作,当应用协程的时候,咱们在线程内应用协程将操作挂起,期待 IO 实现时再继续执行,这样不会产生线程切换等操作。
- 简化异步编程:在咱们应用 rpc 框架时,框架往往会提供同步,异步等调用形式,当同步调用其余接口时,以后线程会被阻塞,当异步调用其余接口时,就须要你提供一个回调函数,当有后果返回时,由框架将后果回吐给你。这种编程形式是不不便的,协程能够简化这个操作,前面咱们会举例说明。
上面我会介绍协程是如何产生上述劣势的,行文逻辑如下,在第二个章节,我会介绍,当已知了协程的性能,应用协程的时候,咱们如何简化了异步编程;第三个章节咱们会介绍协程是如何实现咱们心愿的那些性能的。
二 应用协程异步编程
应用异步做网络编程 (实现业务逻辑) 时,咱们的业务代码是有严格的执行程序的,然而异步的返回是无序的,就使得咱们,代码往往须要一些状态码来判断前置调用是否曾经实现,如果再叠加了异样解决这些逻辑的话,代码逻辑会十分艰涩难懂,而且容易经常性的造成回调天堂。举个例子,如果咱们应用异步回调的形式对一个整型数字做加 3 的操作,咱们有一个加 1 的函数,加 3 时须要调用三次:
void AsyncAddOne(int val, std::function<void (int)> 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<Result> CommonHandler() {
// 1. do something in common handler
return MakeReadyFuture<Result1>(res);
}
void HttpHandler() {
// 1. 解决公共逻辑
auto http_task = CommonHandler();
// 2. 工作 1 实现后,创立并执行并行任务
auto data_task = http_task.Then([](Future<Result1>&& result1) {
// 2.1 创立 redis 工作,通过 redis_proxy 发动调用, 并返回相干后果,cmd 为申请 redis 的命令
trpc::Future<Result2> fut_redis_task = redis_proxy->AsyncRedis(cmd);
// 2.2 创立 mysql 工作, 通过 mysql_proxy 发动调用, 并返回相干后果,cmd 为申请 mysql 的命令
trpc::Future<Result2> 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<Future<Result2>>&& result2) {
// 别离取得 redis 和 mysql 的 result, 进而实现相干工作
// result[0].GetValue();
// result[1].GetValue();
// 3. do something calc handler...
return trpc::MakeReadyFuture<Resul3t>(res);
});
return fut;
});
// 回包
data_task.Then([](Future<Result3>&& 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 做一些剖析(未完待续)