開發工具:Visual Studio 2019
概念
協程,是一種比線程更加輕量級的存在,協程不是被操作系統內核所管理,而完全是由程序所控制(也就是在用戶態執行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。
協程的特點在於是一個線程執行,那和多線程比,協程有何優勢?
- 極高的執行效率:因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯
- 不需要多線程的鎖機制:因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
缺點
- 無法利用多核資源:協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上.當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是cpu密集型應用。
- 進行阻塞(Blocking)操作(如IO時)會阻塞掉整個程序
參考資料:https://blog.csdn.net/Woosual/article/details/107930147
CPU密集型代碼(各種循環處理、計算等等):不需要頻繁的切換線程,所以多線程是一個不錯的選擇。
IO密集型代碼(文件處理、網絡爬蟲等):尤其是高並發時。為了保證公平,時間片的分配會越來越小,切換越發頻繁。資源也就被浪費在了上下文切換中。為了解決 I/O 密集型運算內核在資源調度上的缺陷,所以引入了協程(coroutine)的概念。
如果在C++20的一個函數體內包含co_await、co_yield、co_return中任何一個關鍵字,那么這個函數就是一個coroutine。其中:
- co_await:掛起當前的coroutine。
- co_return:從當前coroutine返回一個結果。
- co_yield:返回一個結果並且掛起當前的coroutine。
參考資料:https://blog.csdn.net/github_18974657/article/details/108526591
摘點關鍵的
了解了協程后我們就可以發現了以下事實:
一個線程只能有一個協程
協程函數需要返回值是Promise
協程的所有關鍵字必須在協程函數中使用
在協程函數中可以按照同步的方式去調用異步函數,只需要將異步函數包裝在Awaitable類中,使用co_wait關鍵字調用即可。
知道了以上事實,我們就可以按照以下方式使用協程了:
在一個線程中同一個時間只調用一個協程函數,即只有一個協程函數執行完畢了,再去調用另一個協程函數。
使用Awatiable類包裝所有的異步函數,一個異步函數處理一請求中的一部分工作(比如執行一次SQL查詢,或者執行一次http請求等)。
在對應的協程函數中按照需要,通過增加co_wait關鍵字同步的調用這些異步函數。注意一個異步函數(包裝好的Awaiable類)可以在多個協程函數中調用,協程函數可能在多個線程中被調用(雖然一個線程同一時間只調用一個協程函數),所以最好保證Awaiable類是線程安全的,避免出現需要加鎖的情況。
在線程中通過調用不同的協程函數響應不同的請求。
代碼
參考資料:https://blog.csdn.net/zhudonghe1/article/details/107035757
一個協程對象的完整結構如下
struct action // 名稱任意, 系統庫提供了suspend_never, suspend_always, suspend_if可供調用
{
bool await_ready() noexcept { return false; } // 必須實現此接口
void await_suspend(coroutine_handle<>) noexcept {} // 必須實現此接口, 可通過此處在函數內部獲取到handle
void await_resume() noexcept {} // 必須實現此接口
}
template <typename ToOut, typename ToIn> // 非必須
struct coroutine_name // 名稱任意
{
struct promise_type // 名稱必須為promise_type
{
ToOut _to_out; // 非必須, 名稱任意
ToIn _to_in; // 非必須, 名稱任意
promise_type() = default; // 非必須
~promise_type() = default; // 非必須
coroutine_name get_return_object() // 必須實現此接口
{
return std::coroutine_handle<promise_type>::from_promise(*this);
}
auto initial_suspend() // 必須實現此接口, 返回值必須為類似action的struct
{
}
auto final_suspend() // 必須實現此接口, 返回值必須為類似action的struct
{
}
void unhandled_exception() // 必須實現此接口, 用於處理協程函數內部拋出錯誤
{
}
auto yield_value(ToOut val) // 如果協程函數內部有關鍵字co_yield則必須實現此接口, 返回值必須為類似action的struct
{
}
void return_void() // 如果協程函數內部無關鍵字co_return則必須實現此接口
{
}
void return_value(ToOut val) // 如果協程函數內部有關鍵字co_return則必須實現此接口
{
_to_out = val;
}
}
using promise_type = promise_name; // 非必須,只是起別名,可以代替 coroutine_name 結構體中以下代碼中的 promise_type
std::coroutine_handle<promise_type> handle; // 非必須, 但一般均需實現, 名稱隨意, 提供給外面的handle
coroutine_name(std::coroutine_handle<promise_type> p) : handle(coroutine_handle<promise_type>::from_promise(p))
{
};
}
coroutine_name func() // 協程函數
{
co_await suspend_always{};
co_yield val;
co_return val;
}
auto operator co_await(val_type &val) noexcept
{
do something...
return action{};
}
int main()
{
auto f = func();
f.handle.resume(); //用得最多
f.handle.promise()._to_out; //用得較多
f.handle.done(); //用得較少
f.handle.destory(); //一般不用
}
promise是C++對應協程規范的一種數據類型,里面有多個成員函數。通過它們,用戶可以自定義協程的行為,如何時暫停、返回等
get_return_object // to create return object
initial_suspend // entering the coroutine body
return_value // called when co_return called
return_void // called before the end of coroutine body
yield_value // called when co_yield called
final_suspend // called when coroutine ends
unhandled_exception // handle exception
wait_ready:返回 Awaitable 實例是否已經 ready 。協程開始會調用此函數,如果返回true,表示你想得到的結果已經得到了,協程不需要執行了。所以大部分情況這個函數的實現是要 return false。
await_suspend:掛起 awaitable 。該函數會傳入一個 coroutine_handle 類型的參數。這是一個由編譯器生成的變量。在此函數中調用 handle.resume(),就可以恢復協程。
await_resume:當協程重新運行時,會調用該函數。這個函數的返回值就是 co_await 運算符的返回值。
大致執行流程可以通過調試知道,第一步是調用 get_return_object
(所以我們在這個函數實現中要創建返回對象),協程進入 initial_suspend
-> 協程函數體 -> final_suspend
協程完全結束。
函數體中遇到 co_return 則調用 return_value。
在 initial_suspend
和 final_suspend
函數中可以通過 return true or false 來決定是否暫停。
再來個文檔資料吧
cppreference:https://en.cppreference.com/w/cpp/language/coroutines
再來個油管視頻
Andreas Buhr: C++ Coroutines:https://youtu.be/vzC2iRfO_H8
我寫的一個小例子,實現的是 C# 的 ContinueWith
,算不上真正的異步
我覺得寫的挺爛的,就算用 thread::swap()
也可以更簡單的實現,沒什么參考價值
#include<iostream>
#include<future>
#include<thread>
#include<string>
#include<sstream>
#include<windows.h>
#include<coroutine>
#include<stdexcept>
#include<functional>
//獲取線程ID
unsigned long long GetThreadId(std::thread::id tid)
{
std::ostringstream oss;
oss << tid;
std::string stid = oss.str();
return std::stoull(stid);
}
//輸出字符串+線程ID
void Print(std::string s)
{
std::string str = s + std::to_string(GetThreadId(std::this_thread::get_id())) + "\n\n";
std::cout << str;
}
//用於追蹤
//Just a little helper for debugging
struct LifetimeInspector
{
LifetimeInspector(std::string s) :s(s)
{
std::cout << "Start: " << s << std::endl;
}
~LifetimeInspector()
{
std::cout << "End: " << s << std::endl;
}
std::string s;
};
/// <summary>
/// Task
/// The minimal machinery to use c++ 20 coroutines
/// </summary>
template<typename T>
struct Task
{
/// <summary>
/// TaskPromise:
/// The minimal example coroutine
/// </summary>
struct TaskPromise
{
// to create return object
Task get_return_object()
{
//from_promise 從協程的承諾對象創建 coroutine_handle
return std::coroutine_handle<TaskPromise>::from_promise(*this);
}
//suspend_never 是空類,能用於指示 await 表達式絕不暫停並且不產生值
// entering the coroutine body
auto initial_suspend()
{
return std::suspend_never{};
}
//suspend_always 是空類,能用於指示 await 表達式始終暫停並且不產生值
auto final_suspend()
{
return std::suspend_always{};
}
void return_value(T value)
{
//std::cout << "got " << value << "\n";
}
void unhandled_exception()
{
}
};
using promise_type = TaskPromise;
//類模板 coroutine_handle 能用於指代暫停或執行的協程。
std::coroutine_handle<TaskPromise> handle;
Task(std::coroutine_handle<TaskPromise> h) :handle(h)
{
}
};
template<typename T>
struct Awaitable
{
// _init 和 _result 沒必要
T _init;
T _result;
std::thread* _thread;
Awaitable(T init, std::thread& t)
{
this->_init = init;
this->_result = init;
this->_thread = &t;
}
bool await_ready() const
{
return false;
}
T await_resume()
{
return this->_result;
}
void await_suspend(std::coroutine_handle<> handle)
{
//這里將協程句柄交給另外的線程,這樣 co_await 之后的代碼就會在新的線程上運行
*this->_thread = std::thread([handle]()
{
handle.resume();
});
Print("Current Thread ID:");
std::string str = "New Thread ID:" + std::to_string(GetThreadId(this->_thread->get_id())) + "\n\n";
std::cout << str;
}
};
Awaitable<int> SwitchToNewThread(std::thread& t)
{
LifetimeInspector l("SwitchToNewThread");
Awaitable<int> awaitable(100, t);
//do something
return awaitable;
}
Task<int> ResumingOnNewThread(std::thread& t)
{
LifetimeInspector l("ResumingOnNewThread");
Print("Current Thread ID:");
co_await SwitchToNewThread(t);
Print("After co_await,Current Thread ID:");
co_return 42;
}
int main()
{
DWORD start = GetTickCount64();
LifetimeInspector l("main");
Print("Current Thread ID:");
std::thread t;
ResumingOnNewThread(t);
std::cout << "Done\n";
DWORD end = GetTickCount64();
std::cout << end - start << std::endl;
Print("After Done,Current Thread ID:");
t.join();
//t.detach();
return 0;
}
結果,可以看到 co_await
之后的代碼確實是在新的線程上運行
注意:是 co_await
之后的代碼在新線程上運行
代碼執行順序,通過調試觀察
main()
-> ResumingOnNewThread()
函數開頭 -> get_return_object()
-> Task()
構造器 -> get_return_object()
-> 回到 ResumingOnNewThread()
函數開頭 -> initial_suspend()
-> 回到 ResumingOnNewThread()
函數開頭 -> ResumingOnNewThread()
正常運行到 co_await SwitchToNewThread()
-> SwitchToNewThread()
函數 -> Awaitable()
構造器 -> SwitchToNewThread()
函數執行完畢返回 co_await SwitchToNewThread()
處 -> await_ready()
-> 返回 co_await SwitchToNewThread()
處 -> await_suspend()
-> 返回 co_await SwitchToNewThread()
處 之后正常運行
C++ 20 Coroutine 協程 結束
我主要是因為異步才去看 C++ 的協程,並沒有完全掌握,只寫了一個簡單的異步實現例子,想學還是去油管找找吧,或者看一些國外的文檔
但是很遺憾的,C++ 20 並不能像 C# 或其它語言的 aysnc/await 那樣寫出同步式的異步代碼,C++ 20 的協程標准只包含編譯器需要實現的底層功能,並沒有包含簡單方便地使用協程的高級庫,相關的類和函數進入 std 標准庫估計要等到 C++ 23 。所以,在 C++ 20 中,如果要使用協程,要么等別人封裝好了給你用,要么就要自己學着用底層的功能自己封裝。
所以 C++ 真正的異步標准庫和規范要等到 C++ 23 了,三年又三年 😄