天天看點

Google“戰敗”後,C++20 用微軟的提案進入協程時代

【CSDN 編者按】兩年前,C++20 正式釋出。在這一版本,開發者終于迎來了協程特性,它可以讓代碼非常清爽,簡單易懂,同時保持了異步的高性能。但不少開發者直言,C++的協程标準是給庫的開發者使用的,非常複雜,對普通開發者一點都不友好。在這篇文章中,C++ 資深技術專家祁宇立足于 C++20 使用的無棧協程标準,以具體示例分享協程的具體應用實踐與經驗。

作者 | 祁宇,許傳奇,韓垚 責編 | 屠敏

出品 | CSDN(ID:CSDNnews)

經過多年的醞釀、争論、準備後,協程終于進入 C++20 标準。

Google“戰敗”後,C++20 用微軟的提案進入協程時代
Google“戰敗”後,C++20 用微軟的提案進入協程時代

微軟提出并主導的無棧協程成為C++20協程标準

協程并不是一個新的概念,它距今已經有幾十年的曆史了,也早已存在于許多其它程式設計語言(Python、C#、Go)。

協程分為無棧協程和有棧協程兩種,無棧指可挂起/恢複的函數,有棧協程則相當于使用者态線程。有棧協程切換的成本是使用者态線程切換的成本,而無棧協程切換的成本則相當于函數調用的成本。

無棧協程和線程的差別:無棧協程隻能被線程調用,本身并不搶占核心排程,而線程則可搶占核心排程。

C++20 協程中采納的是微軟提出并主導(源于 C#)的無棧協程。很多人反對這個特性,主要槽點包括:難于了解、過于靈活、動态配置設定導緻的性能問題等等。Google 對該提案發起了一系列吐槽并嘗試給出了有棧協程的方案。有棧協程比系統級線程輕量很多,但比起無棧協程還是差了許多。

由于 C++ 的設計哲學是"Zero Overhead Abstractions",最終無棧協程成為了 C++20 協程标準。

當今 C++ 世界演化的兩大主旋律是異步化與并行化。而 C++20 協程能夠以同步文法寫異步代碼的特性,使其成為編寫異步代碼的好工具,異步庫的協程化将是大勢所趨,是以很有必要掌握 C++20 協程。

通過一個簡單的例子來展示一下協程的“妙處”。

async_resolve({host, port}, (auto endpoint){
async_connect(endpoint, (auto error_code){
async_handle_shake((auto error_code){
send_data_ = build_request;

async_write(send_data_, (auto error_code){
async_read;
});
});
});
});

voidasync_read {
async_read(response_, (auto error_code){
if(!finished) {
append_response(recieve_data_);
async_read;
}else {
std::cout<<"finished ok\n";
}
});
}           

基于回調的異步client的僞代碼

基于異步回調的 client 流程如下:

  • 異步域名解析
  • 異步連接配接
  • 異步 SSL 握手
  • 異步發送資料
  • 異步接收資料

這個代碼有很多回調函數,使用回調的時候還有一些陷阱,比如如何保證安全的回調、如何讓異步讀實作異步遞歸調用,如果再結合異步業務邏輯,回調的嵌套層次會更深,我們已經看到 callback hell 的影子了!可能也有讀者覺得這個程度的異步回調還可以接受,但是如果工程變大,業務邏輯變得更加複雜,回調層次越來越深,維護起來就很困難了。

再來看看用協程是怎麼寫這個代碼的:

auto endpoint = co_await async_query({host, port});
auto error_code = co_await async_connect(endpoint);
error_code = co_await async_handle_shake;
send_data = build_request;
error_code = co_await async_write(send_data);
while(true) {
co_await async_read(response);
if(finished) {
std::cout<<"finished ok\n";
break;
}

append_response(recieve_data_);
}           

基于C++20協程的異步client

同樣是異步 client,相比回調模式的異步 client,整個代碼非常清爽,簡單易懂,同時保持了異步的高性能,這就是 C++20 協程的威力!

相信你看了這個例子之後應該不會再想用異步回調去寫代碼了吧,是時候擁抱協程了!

Google“戰敗”後,C++20 用微軟的提案進入協程時代

C++20 為什麼選擇無棧協程?

有棧(stackful)協程通常的實作手段是在堆上提前配置設定一塊較大的記憶體空間(比如 64K),也就是協程所謂的“棧”,參數、return address 等都可以存放在這個“棧”空間上。如果需要協程切換,那麼通過 swapcontext 一類的形式來讓系統認為這個堆上空間就是普通的棧,這就實作了上下文的切換。

有棧協程最大的優勢就是侵入性小,使用起來非常簡便,已有的業務代碼幾乎不需要做什麼修改,但是 C++20 最終還是選擇了使用無棧協程,主要出于下面這幾個方面的考慮。

  • 棧空間的限制

有棧協程的“棧”空間普遍是比較小的,在使用中有棧溢出的風險;而如果讓“棧”空間變得很大,對記憶體空間又是很大的浪費。無棧協程則沒有這些限制,既沒有溢出的風險,也無需擔心記憶體使用率的問題。

  • 性能

有棧協程在切換時确實比系統線程要輕量,但是和無棧協程相比仍然是偏重的,這一點雖然在我們目前的實際使用中影響沒有那麼大(異步系統的使用通常伴随了 IO,相比于切換開銷多了幾個數量級),但也決定了無棧協程可以用在一些更有意思的場景上。舉個例子,C++20 coroutines 提案的作者 Gor Nishanov 在 CppCon 2018 上示範了無棧協程能做到納秒級的切換,并基于這個特點實作了減少 Cache Miss 的特性。

無棧協程是普通函數的泛化

無棧協程是一個可以暫停和恢複的函數,是函數調用的泛化。

為什麼?

我們知道一個函數的函數體(function body)是順序執行的,執行完之後将結果傳回給調用者,我們沒辦法挂起它并稍後恢複它,隻能等待它結束。而無棧協程則允許我們把函數挂起,然後在任意需要的時刻去恢複并執行函數體,相比普通函數,協程的函數體可以挂起并在任意時刻恢複執行。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

是以,從這個角度來說,無棧協程是普通函數的泛化。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

C++20 協程的“微言大義”

C++20 提供了三個新關鍵字(co_await、co_yield 和 co_return),如果一個函數中存在這三個關鍵字之一,那麼它就是一個協程。

編譯器會為協程生成許多代碼以實作協程語義。會生成什麼樣的代碼?我們怎麼實作協程的語義?協程的建立是怎樣的?co_await機制是怎樣的?在探索這些問題之前,先來看看和 C++20 協程相關的一些基本概念。

協程相關的對象

協程幀(coroutine frame)

當 caller 調用一個協程的時候會先建立一個協程幀,協程幀會建構 promise 對象,再通過 promise 對象産生 return object。

協程幀中主要有這些内容:

  • 協程參數
  • 局部變量
  • promise 對象

這些内容在協程恢複運作的時候需要用到,caller 通過協程幀的句柄 std::coroutine_handle 來通路協程幀。

promise_type

promise_type 是 promise 對象的類型。promise_type 用于定義一類協程的行為,包括協程建立方式、協程初始化完成和結束時的行為、發生異常時的行為、如何生成 awaiter 的行為以及 co_return 的行為等等。promise 對象可以用于記錄/存儲一個協程執行個體的狀态。每個協程桢與每個 promise 對象以及每個協程執行個體是一一對應的。

coroutine return object

它是promise.get_return_object方法建立的,一種常見的實作手法會将 coroutine_handle 存儲到 coroutine object 内,使得該 return object 獲得通路協程的能力。

std::coroutine_handle

協程幀的句柄,主要用于通路底層的協程幀、恢複協程和釋放協程幀。程式員可通過調用 std::coroutine_handle::resume 喚醒協程。

co_await、awaiter、awaitable

  • co_await:一進制操作符;
  • awaitable:支援 co_await 操作符的類型;
  • awaiter:定義了 await_ready、await_suspend 和 await_resume 方法的類型。

co_await expr 通常用于表示等待一個任務(可能是 lazy 的,也可能不是)完成。co_await expr 時,expr 的類型需要是一個 awaitable,而該 co_await表達式的具體語義取決于根據該 awaitable 生成的 awaiter。

看起來和協程相關的對象還不少,這正是協程複雜又靈活的地方,可以借助這些對象來實作對協程的完全控制,實作任何想法。但是,需要先要了解這些對象是如何協作的,把這個搞清楚了,協程的原理就掌握了,寫協程應用也會遊刃有餘了。

協程對象如何協作

以一個簡單的代碼展示這些協程對象如何協作:

Return_t foo  { 
auto res = co_await awaiter;
co_return res ;
}
           

Return_t:promise return object。

awaiter: 等待一個task完成。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

協程運作流程圖

圖中淺藍色部分的方法就是 Return_t 關聯的 promise 對象的函數,淺紅色部分就是 co_await 等待的 awaiter。

這個流程的驅動是由編譯器根據協程函數生成的代碼驅動的,分成三部分:

  • 協程建立;
  • co_await awaiter 等待 task 完成;
  • 擷取協程傳回值和釋放協程幀。

協程的建立

Return_t foo  { 
auto res = co_await awaiter;
co_return res ;
}
           

foo協程會生成下面這樣的模闆代碼(僞代碼),協程的建立都會産生類似的代碼:

{
co_await promise.initial_suspend;
try
{
coroutine body;
}
catch (...)
{
promise.unhandled_exception;
}
FinalSuspend:
co_await promise.final_suspend;
}
           

首先需要建立協程,建立協程之後是否挂起則由調用者設定 initial_suspend 的傳回類型來确定。

建立協程的流程大概如下:

  • 建立一個協程幀(coroutine frame)
  • 在協程幀裡建構 promise 對象
  • 把協程的參數拷貝到協程幀裡
  • 調用 promise.get_return_object 傳回給 caller 一個對象,即代碼中的 Return_t 對象

在這個模闆架構裡有一些可定制點:如 initial_suspend、final_suspend、unhandled_exception 和 return_value。

我們可以通過 promise 的 initial_suspend 和 final_suspend 傳回類型來控制協程是否挂起,在 unhandled_exception 裡處理異常,在 return_value 裡儲存協程傳回值。

可以根據需要定制 initial_suspend 和 final_suspend 的傳回對象來決定是否需要挂起協程。如果挂起協程,代碼的控制權就會傳回到caller,否則繼續執行協程函數體(function body)。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

另外值得注意的是,如果禁用異常,那麼生成的代碼裡就不會有 try-catch。此時協程的運作效率幾乎等同非協程版的普通函數。這在嵌入式場景很重要,也是協程的設計目的之一。

co_await 機制

co_await 操作符是 C++20 新增的一個關鍵字,co_await expr 一般表示等待一個惰性求值的任務,這個任務可能在某個線程執行,也可能在 OS 核心執行,什麼時候執行結束不知道,為了性能,我們又不希望阻塞等待這個任務完成,是以就借助 co_await 把協程挂起并傳回到 caller,caller 可以繼續做事情,當任務完成之後協程恢複并拿到 co_await 傳回的結果。

是以 co_await 一般有這幾個作用:

  • 挂起協程;
  • 傳回到 caller;
  • 等待某個任務(可能是 lazy 的,也可能是非 lazy 的)完成之後傳回任務的結果。

編譯器會根據 co_await expr 生成這樣的代碼:

{
auto&& value = <expr>;
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
if (!awaiter.await_ready) //是否需要挂起協程
{
using handle_t = std::experimental::coroutine_handle<P>;

using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));

<suspend-coroutine> //挂起協程

if constexpr(std::is_void_v<await_suspend_result_t>)
{
awaiter.await_suspend(handle_t::from_promise(p)); //異步(也可能同步)執行task
<return-to-caller-or-resumer> //傳回給caller
}
else
{
static_assert(
std::is_same_v<await_suspend_result_t, bool>,
"await_suspend must return 'void' or 'bool'.");

if (awaiter.await_suspend(handle_t::from_promise(p)))
{
<return-to-caller-or-resumer>
}
}

<resume-point> //task執行完成,恢複協程,這裡是協程恢複執行的地方
}

return awaiter.await_resume; //傳回task結果
}           

這個代碼執行流程就是“協程運作流程圖”中粉紅色部分,從這個生成的代碼可以看到,通過定制 awaiter.await_ready 的傳回值就可以控制是否挂起協程還是繼續執行,傳回 false 就會挂起協程,并執行 awaiter.await_suspend,通過 awaiter.await_suspend 的傳回值來決定是傳回 caller 還是繼續執行。

正是 co_await 的這種機制是變“異步回調”為“同步”的關鍵。

C++20 協程中最重要的兩個對象就是 promise 對象(恢複協程和擷取某個任務的執行結果)和 awaiter(挂起協程,等待task執行完成),其它的都是“工具人”,要實作想要的的協程,關鍵是要設計如何讓這兩個對象協作好。

關于co_await的更多細節,讀者可以看這個文檔(https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await)。

微言大義

再回過頭來看這個簡單的協程:

Return_t foo  { 
auto res = co_await awaiter;
co_return res ;
}           

foo 協程隻有三行代碼,但它最終生成的是一百多行的代碼, 如論是協程的建立還是 co_await 機制都是由這些代碼實作的,這就是 C++20 協程的“微言大義”。

關于 C++20 協程的概念和實作原理已經講了很多了,接下來通過一個簡單的 C++20 協程示例來展示協程是如何運作的。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

一個簡單的 C++20 協程例子

這個例子很簡單,通過 co_await 把協程排程到一個線程中列印一下線程 id。

#include <coroutine>
#include <iostream>
#include <thread>

namespace Coroutine {
struct task {
struct promise_type {
promise_type {
std::cout << "1.create promie object\n";
}
task get_return_object {
std::cout << "2.create coroutine return object, and the coroutine is created now\n";
return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend {
std::cout << "3.do you want to susupend the current coroutine?\n";
std::cout << "4.don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
return {};
}
std::suspend_never final_suspend noexcept {
std::cout << "13.coroutine body finished, do you want to susupend the current coroutine?\n";
std::cout << "14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
return {};
}
voidreturn_void {
std::cout << "12.coroutine don't return value, so return_void is called\n";
}
voidunhandled_exception {}
};

std::coroutine_handle<task::promise_type> handle_;
};

struct awaiter {
boolawait_ready {
std::cout << "6.do you want to suspend current coroutine?\n";
std::cout << "7.yes, suspend becase awaiter.await_ready return false\n";
return false;
}
voidawait_suspend(
std::coroutine_handle<task::promise_type> handle) {
std::cout << "8.execute awaiter.await_suspend\n";
std::thread([handle] mutable { handle; }).detach;
std::cout << "9.a new thread lauched, and will return back to caller\n";
}
voidawait_resume {}
};

task test {
std::cout << "5.begin to execute coroutine body, the thread id=" << std::this_thread::get_id << "\n";//#1
co_await awaiter{};
std::cout << "11.coroutine resumed, continue execcute coroutine body now, the thread id=" << std::this_thread::get_id << "\n";//#3
}
}// namespace Coroutine

intmain {
Coroutine::test;
std::cout << "10.come back to caller becuase of co_await awaiter\n";
std::this_thread::sleep_for(std::chrono::seconds(1));

return 0;
}
           

測試輸出:

1.create promie object
2.create coroutine return object, and the coroutine is created now
3.do you want to susupend the current coroutine?
4.don't suspend because return std::suspend_never, so continue to execute coroutine body
5.begin to execute coroutine body, the thread id=0x10e1c1dc0
6.do you want to suspend current coroutine?
7.yes, suspend becase awaiter.await_ready return false
8.execute awaiter.await_suspend
9.a new thread lauched, and will return back to caller
10.come back to caller becuase of co_await awaiter
11.coroutine resumed, continue execcute coroutine body now, the thread id=0x700001dc7000
12.coroutine don't return value, so return_void is called
13.coroutine body finished, do you want to susupend the current coroutine?
14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye           

從這個輸出可以清晰的看到協程是如何建立的、co_await 等待線程結束、線程結束後協程傳回值以及協程銷毀的整個過程。

協程建立

輸出内容中的 1、2、3 展示了協程建立過程,先建立 promise,再通過 promise.get_return_object 傳回 task,這時協程就建立完成了。

協程建立後的行為

協程建立完成之後是要立即執行協程函數呢?還是先挂起來?這個行為由 promise.initial_suspend 來确定,由于它傳回的是一個 std::suspend_never的awaiter,是以不會挂起協程,于是就立即執行協程函數了。

co_await awaiter

執行協程到函數的 co_await awaiter 時,是否需要等待某個任務?傳回 false 表明希望等待,于是接着進入到 awaiter.wait_suspend,并挂起協程,在 await_suspend 中建立了一個線程去執行任務(注意協程具柄傳入到線程中了,以便後面線上程中恢複協程),之後就傳回到 caller了,caller 這時候可以不用阻塞等待線程結束,可以做其它事情。注意:這裡的 awaiter 同時也是一個 awaitable,因為它支援 co_await。

更多時候我們線上程完成之後才去恢複協程,這樣可以告訴挂起等待任務完成的協程:任務已經完成了,現在可以恢複了,協程恢複後拿到任務的結果繼續執行。

協程恢複

當線程開始運作的時候恢複挂起的協程,這時候代碼執行會回到協程函數繼續執行,這就是最終的目标:在一個新線程中去執行協程函數的列印語句。

協程銷毀

awaiter.final_suspend 決定是否要自動銷毀協程,傳回 std::suspend_never 就自動銷毀協程,否則需要使用者手動去銷毀。

協程的“魔法”

再回過頭來看協程函數:

task test {
std::cout << std::this_thread::get_id << "\n";
co_await awaiter{};
std::cout << std::this_thread::get_id << "\n";
}
           
輸出結果顯示 co_await 上面和下面的線程是不同的,以 co_await 為分界線,co_await 之上的代碼在一個線程中執行,co_await 之下的代碼在另外一個線程中執行,一個協程函數跨了兩個線程,這就是協程的“魔法”。本質是因為在另外一個線程中恢複了協程,恢複後代碼的執行就在另外一個線程中了。
      

另外,這裡沒有展示如何等待一個協程完成,簡單的使用了線程休眠來實作等待的,如果要實作等待協程結束的邏輯,代碼還會增加一倍。

相信你通過這個簡單的例子對 C++20 協程的運作機制有了更深入的了解,同時也會感歎,協程的使用真的隻适合庫作者,普通的開發者想用 C++20 協程還是挺難的,這時就需要協程庫了,協程庫可以大幅降低使用協程的難度。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

為什麼需要一個協程庫

通過前面的介紹可以看到,C++20 協程還是比較複雜的,它的概念多、細節多,又是編譯器生成的模闆架構,又是一些可定制點,需要了解如何和編譯器生成的模闆架構協作,這些對于普通的使用者來說光了解就比較吃力,更逞論靈活運用了。

這時也可以了解為什麼當初 Google 吐槽這樣的協程提案難于了解、過于靈活了,然而它的确可以讓我們僅需要通過定制化一些特定方法就可以随心所欲的控制協程,還是很靈活的。

總之,這就是 C++20 協程,它目前隻适合給庫作者使用,因為它隻提供了一些底層的協程原語和一些協程暫停和恢複的機制,普通使用者如果希望使用協程隻能依賴協程庫,由協程庫來屏蔽這些底層細節,提供簡單易用的 API。是以,我們迫切需要一個基于 C++20 協程封裝好的簡單易用的協程庫。

正是在這種背景下,C++20 協程庫 async_simple(https://github.com/alibaba/async_simple)就應運而生了!

阿裡巴巴開發的 C++20 協程庫,目前廣泛應用于圖計算引擎、時序資料庫、搜尋引擎等線上系統。連續兩年經曆天貓雙十一磨砺,承擔了億級别流量洪峰,具備非常強勁的性能和可靠的穩定性。

async_simple 現在已經在 GitHub 上開源,有了它你在也不用為 C++20 協程的複雜而苦惱了,正如它的名字一樣,讓異步變得簡單。

接下來我們将介紹如何使用 async_simple 來簡化異步程式設計。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

async_simple 讓協程變得簡單

async_simple 提供了豐富的協程元件和簡單易用的 API,主要有:

  1. Lazy:lazy 求值的無棧協程
  2. Executor:協程執行器
  3. 批量操作協程的 API:collectAll 和 collectAny
  4. uthread:有棧協程

關于 async_simple 的更多介紹和示例,可以看 GitHub(https://github.com/alibaba/async_simple/tree/main/docs/docs.cn)上的文檔。

有了這些常用的豐富的協程元件,我們寫異步程式就變得很簡單了,通過之前列印線程 id 例子來展示如何使用 async_simple 來實作它,也可以對比下用協程庫的話,代碼會簡單多少。

#include "async_simple/coro/Lazy.h"
#include "async_simple/executors/SimpleExecutor.h"

Lazy<void> PrintThreadId{
std::cout<<"thread id="<<std::this_thread::get_id<<"\n";
co_return;
}

Lazy<void> TestPrintThreadId(async_simple::executors::SimpleExecutor &executor){
std::cout<<"thread id="<<std::this_thread::get_id<<"\n";
PrintThreadId.via(&executor).detach;
co_return;
}

intmain {
async_simple::executors::SimpleExecutor executor(/*thread_num=*/1);
async_simple::coro::syncAwait(TestPrintThreadId(executor));
return 0;
}
           

借助 async_simple 可以輕松地把協程排程到 executor 線程中執行,整個代碼變得非常清爽,簡單易懂,代碼量相比之前少得多,使用者也不用去關心 C++20 協程的諸多細節了。

借助 async_simple 這個協程庫,可以輕松的讓 C++20 協程這隻“王謝堂前燕,飛入尋常百姓家”!

async_simple 提供了很多 example,比如使用 async_simple 開發 http client、http server、smtp client 等示例,更多 Demo 可以看 async_simple 的 demo example(https://github.com/alibaba/async_simple/blob/main/demo_example)。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

性能

使用 async_simple 中的 Lazy 與 folly 中的 Task 以及 cppcoro 中的 task 進行比較,對無棧協程的建立速度與切換速度進行性能測試。需要說明的是,這隻是一個高度裁剪的測試用于簡單展示 async_simple,并不做任何性能比較的目的。而且 Folly::Task 有着更多的功能,例如 Folly::Task 在切換時會在 AsyncStack 記錄上下文以增強程式的 Debug 便利性。

測試硬體

CPU: Intel® Xeon® Platinum 8163 CPU @ 2.50GHz

測試結果

機關: 納秒,數值越低越好。

Google“戰敗”後,C++20 用微軟的提案進入協程時代
Google“戰敗”後,C++20 用微軟的提案進入協程時代

測試結果表明 async_simple 的性能還是比較出色的,未來還會持續去優化改進。

Google“戰敗”後,C++20 用微軟的提案進入協程時代

總結

C++20 協程像一台精巧的“機器”,雖然複雜,但非常靈活,允許我們去定制化它的一些“零件”,通過這些定制化的“零件”我們可以随心所欲的控制這台“機器”,讓它幫我們實作任何想法。

正是這種複雜性和靈活性讓 C++20 協程的使用變得困難,幸運的是我們可以使用工業級的成熟易用的協程庫 async_simple 來簡化協程的使用,讓異步變得簡單!

參考資料:

  • https://github.com/alibaba/async_simple
  • https://timsong-cpp.github.io/cppwp/n4868/
  • https://blog.panicsoftware.com/coroutines-introduction/
  • https://lewissbaker.github.io/

  • https://juejin.cn/post/6844903715099377672
  • https://wiki.tum.de/download/attachments/93291100/Kolb%20report%20-%20Coroutines%20in%20C%2B%2B20.pdf

作者:祁宇,Modern C++ 開源社群 purecpp.org 創始人,《深入應用 C++11》作者

許傳奇,阿裡巴巴開發工程師, LLVM Committer, C++ 标準委員會成員

韓垚,阿裡巴巴工程師,目前從事搜尋推薦引擎開發

Google“戰敗”後,C++20 用微軟的提案進入協程時代

END

《新程式員001-004》全面上市,對話世界級大師,報道中國IT行業創新創造

Google“戰敗”後,C++20 用微軟的提案進入協程時代

成就一億技術人

Google“戰敗”後,C++20 用微軟的提案進入協程時代