[C++] C++20協程的幾個特性補充

[C++] C++20協程的幾個特性補充

0. 前言

  上文 [C++] C++20協程例子之惰性計算(附時序圖) 已在目前MSVC的具體實現上, 大概描述了一個協程的整個工作流程.
  本文繼續在MSVC的具體實現上, 簡述各個階段中的一些細節.
  與上文同理, 本文依舊用守護器稱呼promise_type, 用同步器稱呼awaiter

1. 協程中間對象生命週期

在這裏插入圖片描述

  虛線爲可選路徑.
  initial_suspend返回的同步器其生命週期會和守護器一起, 伴隨整個協程的生命週期. 其餘同步器只是中途的匆匆過客.
  個人猜測initial_suspend返回的同步器生命週期過長應該是BUG, 未來可能會修復, 請不要過於依賴該特性.
  另外個人還猜測initial_suspend的調用以後可能會延後到守護器調用完構造函數之後.

2. 協程函數的近似實現

  協程函數

Lazy f(int a) {
	for(int i = 0; i < a; ++i) {
		co_yield i;
	}
	co_return 10;
}

近似實現如下

Lazy f(int a) {
	// 協程分配內存
	std::coroutine_traits<Lazy, int>::promise_type promise;
	// 按照C++標準 promise 應先構造再調用 initial_suspend 但 MSVC 的實際實現是顛倒的
	auto initial_awaiter = promise.initial_suspend();
	// initial_awaiter 在主線程棧上普通構造, 然後在協程棧上移動構造, 然後在主線程棧上析構, 最後實爲協程棧上的對象
	promise::promise_type();
	auto __return__ = promise.get_return_object(); // __return__ 在主線程棧上普通構造
	// 該 __return__ 對象將在本協程函數掛起後給主線程函數作爲協程函數的返回值
	co_await initial_awaiter; // 該 initial_awaiter 對象生命週期一直持續到協程結束
	
	try {
		for(int i = 0; i < a; ++i) {
			co_await promise.yield_value(i);
			// co_yield exp 轉化爲等價表達式 co_await promise.yield_value(exp)
		}
		promise.return_value(10);
		goto final_suspend;
		// co_return exp; 轉化爲等價表達式 promise.return_value(exp); goto final_suspend;
		// co_return; 轉化爲等價表達式 promise.return_void(); goto final_suspend;
	} catch(...) {
		promise.unhandled_exception();
		goto final_suspend;
	}

final_suspend:
	co_await promise.final_suspend();
	// initial_awaiter 析構
	// 實參 析構
	// promise 析構
	// 協程回收內存
}

3. co_await運算符的近似實現

  co_await是一個新的單目運算符. 運算符可重載.
  當其作用在一個未重載的對象上時, 例如下

(co_await awaiter)

  表達式近似實現如下

((awaiter.await_ready() ? : awaiter.await_suspend(this_coroutine_handle)), awaiter.await_resume())

  整個表達式的類型由await_resume的返回類型決定.
  當awaiter.await_ready返回false之後, 協程將自身掛起, 並將自身的句柄作爲參數調用awaiter.await_suspend, 以方便未來恢復協程.
  不管有沒有調用awaiter.await_suspend掛起, 調用完awaiter.await_ready()返回true或者協程恢復後, 協程首先調用awaiter.await_resume, 取其返回值作爲co_await表達式的結果.

4. co_yield運算符選擇重載函數

  注意 co_yield exp 表達式的有等價展開式 co_await promise.yield_value(exp).
  如果需要使用co_yield返回多種類型的對象, 可通過在守護器裏提供多個yield_value函數重載來實現.
  下面是重載的例子

		HandleDeliver yield_value(int v) noexcept {
			this->v = v;
			return HandleDeliver(handle);
		}
		HandleDeliver yield_value(float v) noexcept {
			this->v = (int)v;
			return HandleDeliver(handle);
		}

協程函數中co_yield現可接收float型對象:

Lazy f() {
	for(int i = 0; i < 5; ++i) {
		co_yield i;
	}
	co_yield 3.14f;
	co_return 10;
}

5. 異常處理

  出現異常時, 協程中自動生成的catch語句會接收異常對象, 並無參調用unhandled_exception.
  unhandled_exception函數內部需用std::current_exception()獲取該異常, 爾後可轉移該異常或用std::rethrow_exception()重拋和重接異常.
  下面是處理異常的一個例子.

			try {
				std::rethrow_exception(std::current_exception());
			} catch(std::exception& e) {
				std::cout << "exception: " << e.what() << std::endl;
			} catch(...) {
				std::cout << "unknow exception" << std::endl;
				// 若不使用 std::current_exception 獲取該異常, 則有內存泄漏風險, 特別是拋出的是在堆上創建的異常(throw new)
			}

  注意, 協程棧上建立的異常對象不能拋到協程外, 若需拋到協程外, 則該異常需爲在堆上創建(throw new).

throw new std::exception(); // unhandled_exception 可將其拋到協程外
throw std::exception(); // 不能拋到協程外, 須在 unhandled_exception 函數內處理

6. 協程銷燬

  調用final_suspend函數時, 函數若返回suspend_never()對象,(即協程庫中await_ready恆返回true的對象), 則線程不被掛起, 協程連同守護器等正常銷燬並回收內存, 最後轉移運行權給主線程.
  注意此時守護器已被析構, 最終獲取到運行權的主線程無法讀取協程中co_return等的返回值及其他遺留的數據.
  final_suspend可返回suspend_always()(即協程庫中await_ready恆返回false的對象)使線程臨終時做最後一次掛起, 讓主線程有機會讀取遺留的數據.
  final_suspend返回suspend_always()做最後一次掛起之後,主線程需要且只能調用coroutine_handle<>.destroy將協程銷燬,否則將導致內存泄漏.而且由於協程的運行已經結束,而且不能調用coroutine_handle<>.resume以試圖恢復協程!
  據標準, 協程調用suspend_always並掛起後, 調用coroutine_handle<>.resume的行爲爲UB行爲.

6.1 檢查內存泄漏

  可使用CRT提供的機制進行檢查.
  main函數第一行加上

	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

  之後可在任意位置調用_CrtDumpMemoryLeaks()在調試窗口輸出泄漏信息(能輸出包括地址,大小,申請內存的new所在源碼的位置,前16字節內容等), 若無內存泄漏則無輸出.
  下面是一個內存泄漏的例子

int main() {
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	new int[100];

	_CrtDumpMemoryLeaks();
	return 0;
}

7. 協程句柄coroutine_handle

  協程句柄對象的類型位於std::experimental命名空間. 以後可能會移動到std命名空間.

7.1 協程句柄地址

  標準裏並未提到(或者我看漏了)同一個協程每次調用同步器await_suspend函數進行掛起時, 給予的協程句柄是否都爲同一個. 但測試中實際都爲同一個, 即使是調用final_suspend返回的同步器await_suspend函數進行掛起時, 給予的協程句柄依然爲同一個. 由此可以斷定同一個線程每次掛起時通過同步器給予的協程句柄都是同一個.
  提取協程句柄的裸指針可調用address. 從裸指針重建協程句柄對象可使用coroutine_handle<>::from_address靜態函數.
  另外, 可使用coroutine_handle<promise_type>替換coroutine_handle<>獲得一個可訪問守護器協程句柄類型. 亦可使用守護器重建協程句柄. 下面是一個在外部通過協程的守護器來恢復協程的例子.

std::experimental::coroutine_handle<Lazy::promise_type>::from_promise(promise).resume();

7.2 從協程句柄恢復協程的運行

  從主線程調用resume函數, 或直接使用()運算符即可. 例如下

handle.resume() // 1
handle() // 2

7.3 協程句柄與守護器

  通過MSVC提供的頭文件中的源碼可以發現, 協程句柄的地址與守護器的地址差值是固定的, 也就是說, 一個守護器陪伴一個協程.

8. 用於證明的測試代碼與結果(部分)

  給上文中所有函數都加上輸出後, 得到如下結果.
  其中函數名前的數字爲this地址, await_suspend函數名後的數字爲協程句柄address返回的地址.

00735158 Lazy::promise_type::initial_suspend
006FF454 HandleDeliver::HandleDeliver
007351B8 HandleDeliver::HandleDeliver
006FF454 HandleDeliver::~HandleDeliver
00735158 Lazy::promise_type::promise_type
00735158 Lazy::promise_type::get_return_object
006FF7B0 Lazy::Lazy
007351B8 HandleDeliver::await_ready
007351B8 HandleDeliver::await_resume
f()
f(): 0
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
&result: 006FF7B0
>0
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 1
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>1
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 2
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>2
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 3
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>3
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f(): 4
00735158 Lazy::promise_type::yield_value
00735178 HandleDeliver::HandleDeliver
00735178 HandleDeliver::await_ready
00735178 HandleDeliver::await_suspend 00735168
>4
00735178 HandleDeliver::await_resume
00735178 HandleDeliver::~HandleDeliver
f() return
00735158 Lazy::promise_type::return_value
00735158 Lazy::promise_type::final_suspend
0073518C HandleDeliver::HandleDeliver
0073518C HandleDeliver::await_ready
0073518C HandleDeliver::await_suspend 00735168
0073518C HandleDeliver::~HandleDeliver
007351B8 HandleDeliver::~HandleDeliver
00735158 Lazy::promise_type::~promise_type
result.get_return(): 10

  完整測試源碼如下, 歡迎自行驗證.

#include <iostream>
#include <experimental/coroutine>

class HandleDeliver {
	HandleDeliver() = delete;
	HandleDeliver(const HandleDeliver&) = delete;
	HandleDeliver& operator= (const HandleDeliver&) = delete;
	HandleDeliver& operator= (HandleDeliver &&) = delete;

private:
	std::experimental::coroutine_handle<>& phandle;
	bool is_ready;
public:
	HandleDeliver(std::experimental::coroutine_handle<>& phandle, bool is_ready=false) noexcept :
		phandle(phandle),
		is_ready(is_ready) {
		std::cout << this << " " << __FUNCTION__ << std::endl;
	}
	HandleDeliver(HandleDeliver &&self) noexcept :
		phandle(self.phandle) {
		std::cout << this << " " << __FUNCTION__ << std::endl;
	}
	~HandleDeliver() {
		std::cout << this << " " << __FUNCTION__ << std::endl;
	}
	bool await_ready() const noexcept {
		std::cout << this << " " << __FUNCTION__ << std::endl;
		return is_ready;
	}
	void await_suspend(std::experimental::coroutine_handle<> handle) noexcept {
		std::cout << this << " " << __FUNCTION__ << " " << handle.address() << std::endl;
		phandle = handle;
	}
	void await_resume() const noexcept {
		std::cout << this << " " << __FUNCTION__ << std::endl;
	}
};

class Lazy {
	Lazy() = delete;
	Lazy(const Lazy&) = delete;
	Lazy& operator= (const Lazy&) = delete;
	Lazy& operator= (Lazy &&) = delete;

public:
	class promise_type {
		promise_type(const promise_type&) = delete;
		promise_type(promise_type &&) = delete;
		promise_type& operator= (const promise_type&) = delete;
		promise_type& operator= (promise_type &&) = delete;

	private:
		std::experimental::coroutine_handle<> handle;
		int v;
		bool is_overed;

	public:
		HandleDeliver initial_suspend() noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
			return HandleDeliver(handle, false);
		}
		promise_type() noexcept : v(-1), is_overed(false) {
			std::cout << this << " " << __FUNCTION__ << std::endl;
		}
		Lazy get_return_object() noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
			return Lazy(*this);
		}
		HandleDeliver yield_value(int v) noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
			this->v = v;
			return HandleDeliver(handle);
		}
		HandleDeliver yield_value(float v) noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
			this->v = (int)v * 10;
			return HandleDeliver(handle);
		}
		void return_value(int v) noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
			this->v = v;
		}
		HandleDeliver final_suspend() noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
			is_overed = true;
			return HandleDeliver(handle);
		}
		void unhandled_exception() {
			try {
				std::rethrow_exception(std::current_exception());
			} catch(std::exception& e) {
				std::cout << "exception: " << e.what() << std::endl;
			} catch(...) {
				std::cout << "unknow exception" << std::endl;
			}
		}
		~promise_type() noexcept {
			std::cout << this << " " << __FUNCTION__ << std::endl;
		}

	public:
		void resume() const noexcept {
			handle.resume();
		}
		bool overed() const noexcept {
			return is_overed;
		}
		int value() const noexcept {
			return v;
		}
		void destroy() noexcept {
			handle.destroy();
		}
	};

private:
	class iterator_type {
		iterator_type() = delete;
		iterator_type(const iterator_type&) = delete;
		iterator_type& operator= (const iterator_type&) = delete;
		iterator_type& operator= (iterator_type &&) = delete;

	private:
		Lazy &z;

	public:
		explicit iterator_type(Lazy& z) noexcept : z(z) {}
		iterator_type(iterator_type &&self) noexcept : z(self.z) {}
		iterator_type& operator++ () noexcept {
			z._next();
			return *this;
		}
		bool operator!= (iterator_type const& other) const noexcept {
			return !z.is_overed;
		}
		int operator* () const noexcept {
			return z._get();
		}
	};

private:
	promise_type & promise;
	bool is_overed;
	int retv;

protected:
	bool _next() {
		if(is_overed) {
			return false;
		}
		promise.resume();
		// promise.rethrow_if_has_exception();
		if(promise.overed()) {
			retv = promise.value();
			promise.destroy();
			is_overed = true;
			return false;
		}
		return true;
	}
	int _get() const noexcept {
		if(is_overed) {
			return -1;
		} else {
			return promise.value();
		}
	}

public:
	explicit Lazy(promise_type& promise) noexcept :
		promise(promise),
		is_overed(false),
		retv() {
		std::cout << this << " " << __FUNCTION__ << std::endl;
	}
	Lazy(Lazy &&self) noexcept :
		promise(self.promise),
		is_overed(self.is_overed) {
		std::cout << this << " " << __FUNCTION__ << std::endl;
	}
	iterator_type begin() {
		return iterator_type(*this);
	}
	iterator_type end() {
		return iterator_type(*this);
	}
	int get_return() {
		if(is_overed) {
			return retv;
		} else {
			return -1;
		}
	}
};

Lazy f() {
	std::cout << "f()" << std::endl;
	for(int i = 0; i < 5; ++i) {
		std::cout << "f(): " << i << std::endl;
		co_yield i;
	}
	std::cout << "f() return" << std::endl;
	co_return 10;
}

int main() {
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	{
		Lazy result = f();
		std::cout << "&result: " << &result << std::endl;
		for(int i : result) {
			std::cout << ">" << i << std::endl;
		}
		std::cout << "result.get_return(): " << result.get_return() << std::endl;
	}
	_CrtDumpMemoryLeaks();
	getchar();
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章