景
基于跨平台考虑,微信终端很多基础组件使用 C++ 编写,随着业务越来越复杂,传统异步编程模型已经无法满足业务需要。Modern C++ 虽然一直在改进,但一直没有统一编程模型,为了提升开发效率,改善代码质量,我们自研了一套 C++ 协程框架 owl
,用于为所有基础组件提供统一的编程模型。
owl 协程框架目前主要应用于 C++ 跨平台微信客户端内核(Alita)
,Alita 的业务逻辑部分全部用协程实现,相比传统异步编程模型,至少减少了 50% 代码量。Alita 目前已经应用于儿童手表微信、Linux 车机微信、Android 车机微信等多个业务,其中 Linux 车机微信的所有 UI 逻辑也全部用协程实现。
为什么要造轮子?
那么问题来了,既然 C++20 已经支持了协程,业界也有不少开源方案(如 libco、libgo 等),为什么不直接使用?
原因:
- owl 基础库需要支持尽量多的操作系统和架构,操作系统包括:Android、iOS、macOS、Windows、Linux、
RTOS
;架构包括:x86、x86_64、arm、arm64、loongarch64
,目前并没有任何一个方案能直接支持。 - owl 协程自 2019 年初就推出了,而当时 C++20 还未成熟,实际上到目前为止 C++20 普及程度依然不高,公司内部和外部合作伙伴的编译器版本普遍较低,导致目前 owl 最多只能用到 C++14 的特性
- 业界已有方案有不少缺点:
- 大多为后台开发设计,不适用终端开发场景
- 基本只支持 Linux 系统和 x86/x86_64 架构
- 封装层次较低,大多是玩具或 API 级别,并没有达到框架级别
- 在 C++ 终端开发没有看到大规模应用案例
Show me the code
那么协程比传统异步编程到底好在哪里?下面我们结合代码来展示一下协程的优势,同时也回顾一下异步编程模型的演化过程:
假设有一个异步方法 AsyncAddOne
,用于将一个 int 值加 1,为了简单起见,这里直接开一个线程 sleep 100ms 后再回调新的值:
void AsyncAddOne(int value, std::function<void (int)> callback) {
std::thread t([value, callback = std::move(callback)] {
std::this_thread::sleep_for(100ms);
callback(value + 1);
});
t.detach();
}
要调用 AsyncAddOne
将一个 int 值加 3,有三种主流写法:
1、Callback
传统回调方式,代码写起来是这样:
AsyncAddOne(100, [] (int result) {
AsyncAddOne(result, [] (int result) {
AsyncAddOne(result, [] (int result) {
printf("result %d\n", result);
});
});
});
回调有一些众所周知的痛点,如回调地狱、信任问题、错误处理困难、生命周期管理困难等,在此不再赘述。
2、Promise
Promise 解决了 Callback 的痛点,使用 owl::promise 库的代码写起来是这样:
// 将回调风格的 AsyncAddOne 转成 Promise 风格
owl::promise AsyncAddOnePromise(int value) {
return owl::make_promise([=] (auto d) {
AsyncAddOne(value, [=] (int result) {
d.resolve(result);
});
});
}
// Promise 方式
AsyncAddOnePromise(100)
.then([] (int result) {
return AsyncAddOnePromise(result);
})
.then([] (int result) {
return AsyncAddOnePromise(result);
})
.then([] (int result) {
printf("result %d\n", result);
});
很显然,由于消除了回调地狱,代码漂亮多了。实际上 owl::promise 解决了 Callback 的所有痛点,通过使用模版元编程和类型擦除技术,甚至连语法都接近 JavaScript Promise。
但实践发现,Promise 只适合线性异步逻辑,复杂一点的异步逻辑用 Promise 写起来也很乱(如循环调用某个异步接口),因此我们废弃了 owl::promise,最终将方案转向了协程。
3、Coroutine
使用 owl 协程写起来是这样:
// 将回调风格的 AsyncAddOne 转成 Promise 风格
// 注:
// owl::promise 擦除了类型,owl::promise2 是类型安全版本
// owl 协程需要配合 owl::promise2 使用
owl::promise2<int> AsyncAddOnePromise2(int value) {
return owl::make_promise2<int>([=] (auto d) {
AsyncAddOne(value, [=] (int result) {
d.resolve(result);
});
});
}
// Coroutine 方式
// 使用 co_launch 启动一个协程
// 在协程中即可使用 co_await 将异步调用转成同步方式
owl::co_launch([] {
auto value = 100;
for (auto i = 0; i < 3; i++) {
value = co_await AsyncAddOne