VS2019 C++20的coroutines
01 協程(coroutines)
協程就是用戶態線程內可以被異步執行的函數。用來在用戶態下解決異步問題。
普通函數:每次調用只能從第一條語句開始執行直到某個出口結束。
協程函數:協程函數交出控制權後,可以再次從交出控制權的下一語句開始執行。
在協程的叫法出現以前,處理異步問題一般會使用操作系統提供的系統級API來解決異步問題。系統級的API都是大多采用回調函數方式實現。後來人們覺得使用異步回調形式的API比較麻煩,就開始提供異步調用庫或者語言級支持。並且起來個優雅的名字–協同程序。
協程不是線程。協程包含在線程內。協程在用戶態,由用戶控制。協程的切換比線程切換效率高。
同一個線程在一個時間點最多隻能跑一個協程;在同一個線程中,協程的運行是穿行的。索引沒有數據爭用(data race),也不需要鎖。
原則上,應該儘量避免將同一個協程的主體,放到不同的線程中同時執行。因爲這樣有很大概率發生數據爭用(data race)。[其實這種情況下就是線程的數據爭用問題]。所以我們應該在線程中討論協程;而不是在進程中討論協程。
- Boost.Fiber
- Boost.Coroutine
- fiber
- libco
- libtask
- …
協程根據實現方式不同,分爲有棧協程(stackful coroutine)和無棧協程(stackless coroutine)。
有棧協程可通過操作系統提供的系統調用實現;無棧協程需要語言標準和編譯器支持。
OS | 有棧協程系統調用 |
---|---|
Linux | getcontext,setcontext,makecontext,swapcontext |
Windows | CreateFiber,ConvertFiberToThread,SwitchToFiber |
微軟擬提的C++20標準中(目前是ts,即:<experimental/coroutine>)的協程屬於stackless coroutine。
協程函數特點:
1:首次調用協程函數,會從堆中分配一個協程上下文,調用方的返回地址、入口函數、交出控制權等信息保存在協程上下文中。
2:當協程中途交出控制權後,協程上下文不會被刪除(相當於函數退出之後,上下文環境還被保存,類比線程切換)。
3:當協程再次獲得控制權後,會自動從協程上下文中恢復調用環境,然後從上一次交出控制權的下一條語句繼續執行(加載目標協程環境,類比線程切換)。
4:協程函數返回(非中途交出控制權)後,協程上下文將被刪除。
5:若再次調用協程函數,視爲首次調用。
有棧協程(Stackful Coroutines)特點:
1:每個協程都有一個預先分配的調用棧(Call Stack)。
2:每個協程都屬於且僅屬於創建它的線程。
3:一個線程可以包含多個協程。
4:線程本身也可以是一個協程,成爲主協程(Primary Coroutine)。
5:協程必須主動交出控制權,否則同一線程的其它協程均無法獲得執行機會。
6:協程執行路徑上,任何被調用的函數均可在任何位置交出控制權。
7:如果允許協程把控制權交給同一線程的其它協程,則稱爲對稱協程(Symmetry Coroutines)。如果只允許協程把控制權交給主協程,主協程作爲調度器,負責分配執行權,則稱爲非對稱協程(Asymmetry Coroutines)。
8:屬於同一線程的多個協程之間沒有數據爭用(Data Race)問題。
9:無需修改語言標準和編譯器,利用系統調用即可實現。
無棧協程(Stackless Coroutines)特點:
1:每個協程的執行環境,僅需包含調用棧(Call Stack)的頂層棧幀(Top Call Stack Frame),而非整個調用棧,因而空間開銷極小。
2:協程執行路徑上,只有特定語句才能交出控制權。
3:無需協程調度器。
4:調用協程函數時,同一協程函數的不同部分,有可能在不同的線程環境中執行。因此需要處理好數據爭用(Data Race)問題。
5:需要語言標準和編譯器支持。
有棧協程和無棧協程對比,有棧協程的最大缺陷是保存調用棧的開銷太大;
無棧協程不但具有有棧協程的所有優點,而且空間開銷極低;唯一不足就是需要語言標準和編譯器支持。
怎麼識別C++20中的協程
如果在C++20的一個函數體內包含co_await、co_yield、co_return中任何一個關鍵字,那麼這個函數就是一個coroutine。其中:
co_await:掛起當前的coroutine。
co_return:從當前coroutine返回一個結果。
co_yield:返回一個結果並且掛起當前的coroutine。
一個coroutine必定包含Promise和Awaitable組成。
協程通過Promise和Awaitable接口來規範實現。實現最簡單的協程需要用到其中的8個(5個Promise的函數和3個Awaitable的函數)。
如果要實現形如co_await xxxxx;
的協程調用格式, xxxxx
就必須實現Awaitable
。co_await
是一個新的運算符。Awaitable主要有3個函數:
await_ready:返回Awaitable實例是否已經ready。協程開始會調用此函數,如果返回true,表示你想得到的結果已經得到了,協程不需要執行了。所以大部分情況這個函數的實現是要return false。
await_suspend:掛起awaitable。該函數會傳入一個coroutine_handle類型的參數。這是一個由編譯器生成的變量。在此函數中調用handle.resume(),就可以恢復協程。
await_resume:當協程重新運行時,會調用該函數。這個函數的返回值就是co_await運算符的返回值。
02 C++20 Coroutine Demo
啓用ts中的Coroutine功能,需要vs2019中做如下配置:
配置屬性==>常規==>C++語言標準,預覽-最新C++工作草案中的功能(std:c++latest)
配置屬性==>C/C++==>命令行,添加/await
並且在代碼中加入實驗庫#include <experimental/coroutine>
。
下面的demo代碼收集在:
https://github.com/5455945/cpp_demo/tree/master/C%2B%2B20
02.01 coroutine demo01
這是 https://github.com/franktea/temp/blob/master/uncategorized/coroutine.md 中實現的。在這裏對每個函數斷點後,調試,對理解協程流程很有幫助。再結合C++ 協程介紹[譯]中的圖形解釋,會很有幫助。
// https://github.com/franktea/temp/blob/master/uncategorized/co_vs_callback.cpp
// https://github.com/franktea/temp/blob/master/uncategorized/coroutine.md
#include <iostream>
#include <thread>
#include <experimental/coroutine>
#include <chrono>
#include <functional>
#include "co_vs_callback.h"
// clang++ -std=c++2a -fcoroutines-ts -lstdc++ co_vs_callback.cpp
using call_back = std::function<void(int)>;
void Add100ByCallback(int init, call_back f) // 異步調用
{
std::thread t([init, f]() {
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "Add100ByCallback: " << init << std::endl;
f(init + 100);
});
t.detach();
}
struct Add100Awaitable
{
Add100Awaitable(int init) :init_(init) {}
bool await_ready() const { return false; }
int await_resume() { return result_; }
void await_suspend(std::experimental::coroutine_handle<> handle)
{
auto f = [handle, this](int value) mutable {
result_ = value;
handle.resume();
};
Add100ByCallback(init_, f); // 調用原來的異步調用
}
int init_;
int result_;
};
struct Task
{
struct promise_type {
auto get_return_object() { return Task{}; }
auto initial_suspend() { return std::experimental::suspend_never{}; }
auto final_suspend() { return std::experimental::suspend_never{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
};
Task Add100ByCoroutine(int init, call_back f)
{
int ret = co_await Add100Awaitable(init);
//ret = co_await Add100Awaitable(ret);
//ret = co_await Add100Awaitable(ret);
f(ret);
}
void co_vs_callback()
{
//Add100ByCallback(5, [](int value) { std::cout << "get result: " << value << "\n"; });
Add100ByCoroutine(10, [](int value) { std::cout << "get result from coroutine1: " << value << "\n"; });
//Add100ByCoroutine(20, [](int value) { std::cout << "get result from coroutine2: " << value << "\n"; });
//Add100ByCoroutine(30, [](int value) { std::cout << "get result from coroutine3: " << value << "\n"; });
//Add100ByCoroutine(40, [](int value) { std::cout << "get result from coroutine4: " << value << "\n"; });
std::this_thread::sleep_for(std::chrono::seconds(50));
}
02.02 Coroutine demo02 co_await
這個demo來自黑山是不是山黑
的C++ coroutine
// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_await.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;
template<class T>
struct test {
// inner types
struct promise_type;
using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias
// functions
test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
test(const test& s) = delete;
test& operator=(const test&) = delete;
test(test&& s) :handle(s.handle) { s.handle = nullptr; }
test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }
T get()
{
cout << "# Got return value\n";
if (!(this->handle.done()))
{
handle.resume(); //resume
return handle.promise().value;
}
}
struct promise_type
{
promise_type() { cout << "@ promise_type created\n"; }
~promise_type() { cout << "@ promise_type died\n"; }
auto get_return_object() //get return object
{
cout << "@ get_return_object called\n";
return test<T>{handle_type::from_promise(*this)};// pass handle to create "return object"
}
auto initial_suspend() // called before run coroutine body
{
cout << "@ initial_suspend is called\n";
// return std::experimental::suspend_never{}; // dont suspend it
return std::experimental::suspend_always{};
}
auto return_void() // called when just before final_suspend, conflict with return_value
{
cout << "@ return_void is called\n";
return std::experimental::suspend_never{}; // dont suspend it
//return std::experimental::suspend_always{};
}
auto yield_value(int t) // called by co_yield()
{
std::cout << "yield_value called\n";
value = t;
return std::experimental::suspend_always{};
}
auto final_suspend() // called at the end of coroutine body
{
cout << "@ final_suspend is called\n";
return std::experimental::suspend_always{};
}
void unhandled_exception() //exception handler
{
std::exit(1);
}
//T await_transform() {}
// data
T value;
};
// member variables
handle_type handle;
};
struct AwaiableObj
{
int a;
AwaiableObj() :a(0) {}
bool await_ready()
{
cout << "@@ await_ready called\n";
return true;
}
auto await_suspend(std::experimental::coroutine_handle<> awaiting_handle)
{
cout << "@@ await_suspend called\n";
// return ;
// return true;
return false;
// return awaiting_handle;
}
auto await_resume()
{
cout << "@@ await_resume called\n";
return a++;
}
};
test<int> await_routine()
{
auto a = AwaiableObj{};
for (int i = 0; i < 5; i++)
{
auto v = co_await a;
co_yield v;
}
}
void co_vs_await()
{
auto a = await_routine();
auto b = a.get();
cout << "value is " << b << endl;
b = a.get();
cout << "value is " << b << endl;
b = a.get();
cout << "value is " << b << endl;
b = a.get();
cout << "value is " << b << endl;
b = a.get();
cout << "value is " << b << endl;
}
02.03 Coroutine demo02 co_return
這個demo來自黑山是不是山黑
的C++ coroutine
// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_return.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;
template<class T>
struct test {
// inner types
struct promise_type;
using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias
// functions
test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
test(const test& s) = delete;
test& operator=(const test&) = delete;
test(test&& s) :handle(s.handle) { s.handle = nullptr; }
test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }
T get()
{
cout << "# Got return value\n";
if (!(this->handle.done()))
{
handle.resume(); //resume
return handle.promise().value;
}
}
struct promise_type
{
promise_type() { cout << "@ promise_type created\n"; }
~promise_type() { cout << "@ promise_type died\n"; }
auto get_return_object() //get return object
{
cout << "@ get_return_object called\n";
return test<T>{handle_type::from_promise(*this)};// pass handle to create "return object"
}
auto initial_suspend() // called before run coroutine body
{
cout << "@ initial_suspend is called\n";
// return std::experimental::suspend_never{}; // dont suspend it
return std::experimental::suspend_always{};
}
auto return_value(T v) // called when there is co_return expression
{
cout << "@ return_value is called\n";
value = v;
return std::experimental::suspend_never{}; // dont suspend it
//return std::experimental::suspend_always{};
}
auto final_suspend() // called at the end of coroutine body
{
cout << "@ final_suspend is called\n";
return std::experimental::suspend_always{};
}
void unhandled_exception() //exception handler
{
std::exit(1);
}
// data
T value;
};
// member variables
handle_type handle;
};
test<int> return_coroutine()
{
std::cout << "start return_coroutine\n";
co_return 1;
co_return 2; // will never reach here
}
void co_vs_return()
{
auto a = return_coroutine();
cout << "created a corutine, try to get a value\n";
int an = a.get();
cout << "value is " << an << endl;
an = a.get();
cout << "value is " << an << endl;
}
02.04 Coroutine demo02 co_yield
這個demo來自黑山是不是山黑
的C++ coroutine
// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_yield.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;
struct test {
// inner types
struct promise_type;
using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias
// functions
test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
test(const test& s) = delete;
test& operator=(const test&) = delete;
test(test&& s) :handle(s.handle) { s.handle = nullptr; }
test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }
int current_value() {
return handle.promise().value;
}
bool move_next()
{
handle.resume();
return !handle.done();
}
struct promise_type
{
promise_type() { cout << "@ promise_type created\n"; }
~promise_type() { cout << "@ promise_type died\n"; }
auto get_return_object() //get return object
{
cout << "@ get_return_object called\n";
return test{ handle_type::from_promise(*this) };// pass handle to create "return object"
}
auto initial_suspend() // called before run coroutine body
{
cout << "@ initial_suspend is called\n";
return std::experimental::suspend_never{}; // dont suspend it
//return std::experimental::suspend_always{};
}
auto return_void() // called when just before final_suspend, conflict with return_value
{
cout << "@ return_void is called\n";
return std::experimental::suspend_never{}; // dont suspend it
//return std::experimental::suspend_always{};
}
auto yield_value(int t) // called by co_yield()
{
std::cout << "yield_value called\n";
value = t;
return std::experimental::suspend_always{};
}
auto final_suspend() // called at the end of coroutine body
{
cout << "@ final_suspend is called\n";
return std::experimental::suspend_always{};
}
void unhandled_exception() // exception handler
{
std::exit(1);
}
// data
int value;
};
// member variables
handle_type handle;
};
test yield_coroutine(int count)
{
std::cout << "start yield_coroutine\n";
for (int i = 0; i < count; i++)
co_yield i * 2;
}
void co_vs_yield()
{
auto a = yield_coroutine(4);
cout << "created a corutine, try to get a value\n";
do
{
cout << "get value " << a.current_value() << endl;
} while (a.move_next());
}
02.05 librf庫
librf是一個基於C++ Coroutines提案 ‘Stackless Resumable Functions’編寫的非對稱stackless協程庫。
https://github.com/tearshark/librf
參考
1.C++ 協程介紹[譯]
2.C++協程
3.C++ coroutine
4.co_vs_callback
5.isocpp.org的experimental
6.萬字長文 | 漫談libco協程設計及實現