Seastar 教程(三)

morningli發表於2022-03-15

原文:https://github.com/scylladb/seastar/blob/master/doc/tutorial.md

Fiber

Seastar 延續通常很短,但經常相互連結,因此一個延續會做一些工作,然後安排另一個延續以供以後使用。這樣的鏈可能很長,甚至經常涉及迴圈 —— 請參閱下一節“迴圈”。我們將這種鏈稱為執行的“fiber”。

這些fiber不是執行緒——每一個都只是一串延續——但它們與傳統執行緒有一些共同的要求。例如,我們希望避免一根fiber被餓死,而另一根fiber連續不斷地執行它的continuation。作為另一個例子,fiber可能想要進行通訊——例如,一個fiber產生第二個fiber消耗的資料,並且我們希望確保兩個fiber都有機會執行,並且如果一個fiber過早停止,另一個fiber不會永遠掛起。

迴圈

大多數耗時的計算都涉及使用迴圈。Seastar 提供了幾個原語來表達它們,與未來/承諾模型很好地組合在一起。Seastar 迴圈原語的一個非常重要的方面是每次迭代之後都有一個搶佔點,從而允許其他任務在迭代之間執行。

repeat

repeat建立的迴圈執行主體,直到它接收到一個stop_iteration物件,該物件通知迭代應該繼續(stop_iteration::no)還是停止(stop_iteration::yes)。只有在第一個迭代完成後才會啟動下一個迭代。傳遞給repeat的迴圈體應該有一個future<stop_iteration>的返回型別。

seastar::future<int> recompute_number(int number);

seastar::future<> push_until_100(seastar::lw_shared_ptr<std::vector<int>> queue, int element) {
	return seastar::repeat([queue, element] {
		if (queue->size() == 100) {
			return make_ready_future<stop_iteration>(stop_iteration::yes);
		}
		return recompute_number(element).then([queue] (int new_element) {
			queue->push_back(new_element);
			return stop_iteration::no;
		});
	});
}

do_until

do_untilrepeat 的近親,但它使用顯式傳遞的條件來決定是否應該停止迭代。上面的例子可以用do_until表示為:

seastar::future<int> recompute_number(int number);

seastar::future<> push_until_100(seastar::lw_shared_ptr<std::vector<int>> queue, int element) {
	return seastar::do_until([queue] { return queue->size() == 100; }, [queue, element] {
		return recompute_number(element).then([queue] (int new_element) {
			queue->push_back(new_element);
		});
	});
}

請注意,迴圈體應返回future<>,這允許在迴圈內組合複雜的延續。

do_for_each

do_for_each相當於Seastar 世界中的for迴圈。它接受一個範圍(或一對迭代器)和一個函式體,它按順序一個接一個地應用於每個引數。下一次迭代將僅在第一次迭代完成後啟動,就像repeat. 像往常一樣,do_for_each期望它的迴圈體返回一個future<>.

seastar::future<> append(seastar::lw_shared_ptr<std::vector> queue1, seastar::lw_shared_ptr<std::vector> queue2) {
return seastar::do_for_each(queue2, [queue1] (int element) {
queue1->push_back(element);
});
}

seastar::future<> append_iota(seastar::lw_shared_ptr<std::vector<int>> queue1, int n) {
	return seastar::do_for_each(boost::make_counting_iterator<size_t>(0), boost::make_counting_iterator<size_t>(n), [queue1] (int element) {
		queue1->push_back(element);
	});
}

do_for_each接受對容器的左值引用或一對迭代器。這意味著在整個迴圈執行期間確保容器處於活動狀態的責任屬於呼叫者。如果容器需要延長其使用壽命,可以用do_with通過以下方式輕鬆實現:

seastar::future<> do_something(int number);

seastar::future<> do_for_all(std::vector<int> numbers) {
	// Note that the "numbers" vector will be destroyed as soon as this function
	// returns, so we use do_with to guarantee it lives during the whole loop execution:
	return seastar::do_with(std::move(numbers), [] (std::vector<int>& numbers) {
		return seastar::do_for_each(numbers, [] (int number) {
			return do_something(number);
		});
	});
}	

parallel_for_each

parallel_for_eachdo_for_each的高併發變種. 使用 時parallel_for_each,所有迭代都同時排隊—— 這意味著無法保證它們完成操作的順序。

seastar::future<> flush_all_files(seastar::lw_shared_ptr<std::vector<seastar::file>> files) {
	return seastar::parallel_for_each(files, [] (seastar::file f) {
		// file::flush() returns a future<>
		return f.flush();
	});
}

parallel_for_each是一個強大的工具,因為它允許並行生成許多工。這可能是一個巨大的效能提升,但也有一些警告。首先,太高的併發可能會很麻煩——細節可以在限制迴圈的並行性一章中找到。

要限制parallel_for_each的併發性,請使用下面描述的max_concurrent_for_each。有關處理並行性的更多詳細資訊,請參閱限制迴圈的並行性一章。

其次,請注意在parallel_for_each迴圈中執行迭代的順序是任意的——如果需要嚴格的順序,請考慮使用do_for_each

max_concurrent_for_each

max_concurrent_for_eachparallel_for_each有限並行的變體。它接受一個額外的引數——max_concurrent——最多max_concurrent迭代同時排隊,不保證它們以什麼順序完成它們的操作。

seastar::future<> flush_all_files(seastar::lw_shared_ptr<std::vector<seastar::file>> files, size_t max_concurrent) {
	return seastar::max_concurrent_for_each(files, max_concurrent, [] (seastar::file f) {
		return f.flush();
	});
}

確定最大併發限制超出了本文件的範圍。它通常應該源自執行軟體的系統的實際功能,例如並行執行單元或 I/O 通道的數量,以便在不使系統不堪重負的情況下優化資源利用率。

when_all:等待多個future

上面我們已經看到parallel_for_each(),它啟動了一些非同步操作,然後等待所有操作完成。Seastar 有另一個成語,when_all(),用於等待幾個已經存在的期貨完成。

when_all()的第一個變數是可變的,即future作為單獨的引數給出,其確切數量在編譯時是已知的。個別future可能有不同的型別。例如,

#include <seastar/core/sleep.hh>

future<> f() {
	using namespace std::chrono_literals;
	future<int> slow_two = sleep(2s).then([] { return 2; });
	return when_all(sleep(1s), std::move(slow_two), 
					make_ready_future<double>(3.5)
		   ).discard_result();
}

這將啟動三個期貨 —— 一個休眠一秒鐘(並且不返回任何內容),一個休眠兩秒鐘並返回整數 2,以及一個立即返回雙精度 3.5 - 然後等待它們。該when_all()函式返回一個future,它在所有三個future 解析後立即解析,即兩秒後。這個future也有一個值,我們將在下面解釋,但在這個例子中,我們只是等待未來解決並丟棄它的值。

請注意,when_all()只接受右值,它可以是臨時的(如非同步函式的返回值或make_ready_future)或std::move()持有future的變數。

when_all()返回的future為已解析的future 元組,幷包含三個輸入future的結果。繼續上面的例子,

future<> f() {
	using namespace std::chrono_literals;
	future<int> slow_two = sleep(2s).then([] { return 2; });
	return when_all(sleep(1s), std::move(slow_two),
					make_ready_future<double>(3.5)
		   ).then([] (auto tup) {
			std::cout << std::get<0>(tup).available() << "\n";
			std::cout << std::get<1>(tup).get0() << "\n";
			std::cout << std::get<2>(tup).get0() << "\n";
	});
}

該程式的輸出(兩秒後)是1, 2, 3.5:元組中的第一個未來可用(但沒有值),第二個具有整數值 2,第三個是雙精度值 3.5 —— 正如預期的那樣。

一個或多個等待的future可能會在異常中解決,但這不會改變when_all()工作方式:它仍然等待所有期貨解決,每個期貨都有一個值或一個異常,並且在返回的元組中,一些期貨可能包含異常而不是值。例如,

future<> f() {
	using namespace std::chrono_literals;
	future<> slow_success = sleep(1s);
	future<> slow_exception = sleep(2s).then([] { throw 1; });
	return when_all(std::move(slow_success), std::move(slow_exception)
		   ).then([] (auto tup) {
			std::cout << std::get<0>(tup).available() << "\n";
			std::cout << std::get<1>(tup).failed() << "\n";
			std::get<1>(tup).ignore_ready_future();
	});
}

兩個futureavailable()(已解決),但第二個期貨failed()(導致異常而不是值)。注意我們如何在這個失敗的future上呼叫ignore_ready_future(),因為默默地忽略失敗的future被認為是一個錯誤,並將導致“Exceptional future ignored”錯誤訊息。更典型的是,應用程式將記錄失敗的future而不是忽略它。

上面的例子表明正確使用when_all()是不方便和冗長的。結果被包裝在一個元組中,導致冗長的元組語法,並使用就緒的future,必須單獨檢查所有的異常以避免錯誤訊息。

所以Seastar也提供了一個更容易使用的when_all_succeed()功能。此函式也返回一個未來,當所有給定的未來都已解決時,該未來將解決。如果它們都成功了,它將結果值傳遞給 continuation,而不將它們包裝在future或元組中。但是,如果一個或多個future失敗,則when_all_succeed()解析為失敗的future,其中包含來自失敗future之一的異常。如果給定的future不止一個失敗,其中一個將被傳遞(未指定選擇哪一個),其餘的將被靜默忽略。例如,

using namespace seastar;
future<> f() {
	using namespace std::chrono_literals;
	return when_all_succeed(sleep(1s), make_ready_future<int>(2),
					make_ready_future<double>(3.5)
			).then([] (int i, double d) {
		std::cout << i << " " << d << "\n";
	});
}

請注意,future持有的整數和雙精度值是如何方便地單獨(沒有元組)傳遞給continuation的。由於sleep()不包含值,因此等待它,但沒有第三個值傳遞給continuation。這也意味著如果我們when_all_succeed()對幾個future<>(沒有值),結果也是一個future<>:

using namespace seastar;
future<> f() {
	using namespace std::chrono_literals;
	return when_all_succeed(sleep(1s), sleep(2s), sleep(3s));
}

此示例僅等待 3 秒(最大值為 1、2 和 3 秒)。

when_all_succeed()處理異常的一個例子:

using namespace seastar;
future<> f() {
	using namespace std::chrono_literals;
	return when_all_succeed(make_ready_future<int>(2),
					make_exception_future<double>("oops")
			).then([] (int i, double d) {
		std::cout << i << " " << d << "\n";
	}).handle_exception([] (std::exception_ptr e) {
		std::cout << "exception: " << e << "\n";
	});
}

在這個例子中,有一個future失敗了,所以when_all_succeed的結果是一個失敗的future,所以正常的continuation沒有執行,handle_exception() continuation就完成了。

訊號量

Seastar 的訊號量是標準的電腦科學訊號量,適用於future。訊號量是一個計數器,您可以在其中存放或取走單元。如果沒有足夠的單元可用,從計數器取單元可能會等待。

使用訊號量限制並行性

Seastar 中訊號量最常見的用途是限制並行性,即限制可以並行執行的某些程式碼的例項數量。當每個並行呼叫使用有限的資源(例如,記憶體)時,這可能很重要,因此讓無限數量的並行呼叫可能會耗盡該資源。

考慮外部事件源(例如,傳入的網路請求)導致呼叫非同步函式g()的情況。想象一下,我們希望將併發操作g()的數量限制為 100。即,如果 g() 在 100 個其他呼叫仍在進行時啟動,我們希望它延遲其實際工作,直到其他呼叫之一完成。我們可以用訊號量來做到這一點:

seastar::future<> g() {
	static thread_local seastar::semaphore limit(100);
	return limit.wait(1).then([] {
		return slow(); // do the real work of g()
	}).finally([] {
		limit.signal(1);
	});
}

在這個例子中,訊號量從計數器的 100 開始。非同步操作slow()只有在我們可以將計數器減一(wait(1)),這樣,當slow()完成,無論是成功還是異常,計數器會加回一(signal(1))。通過這樣的方式,當100個操作已經開始工作但尚未完成時,第101個操作將等待,直到其中一個正在進行的操作完成並將一個單元返回給訊號量。這確保了每次我們在上述程式碼中最多執行 100個併發操作slow()

請注意我們如何使用static thread_local訊號量,以便g()來自同一分片的所有呼叫都計入相同的限制;像往常一樣,Seastar 應用程式是分片的,因此每個分片(CPU 執行緒)的限制是分開的。這通常很好,因為分片應用程式認為每個分片的資源是分開的。

幸運的是,上面的程式碼恰好是異常安全的:limit.wait(1)可以在記憶體不足時丟擲異常(保留了waiter列表),在這種情況下,訊號量計數器不會減少,但下面的continuation不會執行,所以它不會也增加了。當訊號量broken 時,limit.wait(1)也可以返回一個特殊的future(我們稍後會討論),但在這種情況下,額外的signal()呼叫被忽略。最後,'slow()'也可以丟擲或返回一個異常的未來,但finally()確保訊號量仍然增加。

然而,隨著應用程式程式碼變得越來越複雜,我們都很難確保無論發生在哪個程式碼路徑或異常發生從不會忘記在操作完成後呼叫signal()。作為可能出錯的示例,請考慮以下錯誤程式碼片段,該程式碼片段與上述程式碼片段略有不同,並且乍一看似乎是正確的:

seastar::future<> g() {
	static thread_local seastar::semaphore limit(100);
	return limit.wait(1).then([] {
		return slow().finally([] { limit.signal(1); });
	});
}

但是這個版本不是異常安全的:考慮如果slow()在返回future之前丟擲異常會發生什麼(這與slow()返回異常future不同 —— 我們在異常處理部分討論了這種差異)。在這種情況下,我們減少了計數器,但永遠不會執行到finally(),並且永遠不會增加計數器。有一種方法可以修復此程式碼,方法是將slow()呼叫替換為seastar::futurize_invoke(slow)。但我們在這裡試圖說明的重點不是如何修復有缺陷的程式碼,而是通過使用單獨的semaphore::wait()semaphore::signal()函式,你很容易出錯。

為了異常安全,在 C++ 中一般不建議有單獨的資源獲取和釋放函式。相反,C++ 提供了更安全的機制來獲取資源(在本例中為訊號量單元)並在稍後釋放它:lambda 函式和 RAII(“resource acquisition is initialization”):

基於 lambda 的解決方案是一個函式seastar::with_semaphore(),它是上面示例中程式碼的快捷方式:

seastar::future<> g() {
	static thread_local seastar::semaphore limit(100);
	return seastar::with_semaphore(limit, 1, [] {
		return slow(); // do the real work of g()
	});
}

with_semaphore()和前面的程式碼片段一樣,等待訊號量中給定數量的單元,然後執行給定的 lambda,當 lambda 返回的未來被解析時,with_semaphore()將單元返回給訊號量。with_semaphore()返回一個只有在所有這些步驟完成後才能解決的future

函式seastar::get_units()更通用。它基於 C++ 的 RAII 哲學,為seastar::semaphore 的分開的wait()signal()方法提供了替代方案:該函式返回一個不透明的單位物件,該物件在持有時保持訊號量的計數器減少 —— 一旦該物件被破壞,計數器就會增加回來。使用此介面,您不會忘記增加計數器,或將其增加兩次,或增加而不減少:當建立單位物件時,計數器將始終減少一次,如果成功,則在物件被銷燬時增加。當units物件被移動到一個continuation 中時,無論這個continuation如何結束,當continuation被破壞時,units 物件也被銷燬並且單元被返回到訊號量的計數器。上面用 get_units()編寫的示例如下所示:

seastar::future<> g() {
	static thread_local semaphore limit(100);
	return seastar::get_units(limit, 1).then([] (auto units) {
		return slow().finally([units = std::move(units)] {});
	});
}

請注意get_units()需要使用的有點複雜的方式:continuation必須巢狀,因為我們需要將units物件移動到最後一個continuation。如果slow()返回一個future(並且不立即丟擲),則finally()延續捕獲units物件直到一切完成,但不執行任何程式碼。

Seastars 程式設計師通常應該避免直接使用semaphore::wait()semaphore::signal()函式,並且總是更喜歡with_semaphore()(如果適用)或get_units().

限制資源使用

因為訊號量支援等待任意數量的單元,而不僅僅是 1,所以我們可以將它們用於更多地限制並行呼叫的數量。例如,假設我們有一個非同步函式using_lots_of_memory(size_t bytes),它使用bytes位元組記憶體,並且我們希望確保該函式的所有並行呼叫使用的記憶體不超過 1 MB—— 並且其他呼叫會延遲到之前的呼叫完成了。我們可以用訊號量來做到這一點:

seastar::future<> using_lots_of_memory(size_t bytes) {
	static thread_local seastar::semaphore limit(1000000); // limit to 1MB
	return seastar::with_semaphore(limit, bytes, [bytes] {
		// do something allocating 'bytes' bytes of memory
	});
}

請注意,在上面的示例中,呼叫using_lots_of_memory(2000000)將返回一個永遠不會解析的未來,因為訊號量永遠不會包含足夠的單元來滿足訊號量等待。using_lots_of_memory()應該可能檢查是否bytes超過限制,並在這種情況下丟擲異常。Seastar 不會為您執行此操作。

限制迴圈的並行性

上面,我們檢視了一個被某個外部事件呼叫的函式g(),並希望控制它的並行性。在本節中,我們將研究迴圈的並行性,它也可以通過訊號量來控制。

考慮以下簡單迴圈:

#include <seastar/core/sleep.hh>
seastar::future<> slow() {
	std::cerr << ".";
	return seastar::sleep(std::chrono::seconds(1));
}
seastar::future<> f() {
	return seastar::repeat([] {
		return slow().then([] { return seastar::stop_iteration::no; });
	});
}

此迴圈執行slow()函式(需要一秒鐘才能完成),沒有任何並行性 --- 下一個slow()呼叫僅在前一個呼叫完成時開始。但是,如果我們不需要序列化對slow()的呼叫,並且希望允許它的多個例項同時進行呢?

天真地,我們可以通過在上一次呼叫之後立即開始下一次呼叫來實現更多的並行性slow()--- 忽略上一次呼叫slow()返回的future並且不等待它解決:

seastar::future<> f() {
	return seastar::repeat([] {
		slow();
		return seastar::stop_iteration::no;
	});
}	

但是在這個迴圈中,並行性的數量沒有限制——sleep()在第一個呼叫返回之前,數百萬個呼叫可能是並行活動的。最終,此迴圈可能會消耗所有可用記憶體並崩潰。

使用訊號量允許我們並行執行多個slow()例項,但將這些並行例項的數量限制為,在以下示例中為 100:

seastar::future<> f() {
	return seastar::do_with(seastar::semaphore(100), [] (auto& limit) {
		return seastar::repeat([&limit] {
			return limit.wait(1).then([&limit] {
				seastar::futurize_invoke(slow).finally([&limit] {
					limit.signal(1); 
				});
				return seastar::stop_iteration::no;
			});
		});
	});
}

請注意,此程式碼與我們在上面看到的限制函式g()並行呼叫次數的程式碼有何不同:

  1. 在這裡,我們不能使用單個thread_local訊號量。每個呼叫f()都有其並行度為 100 的迴圈,因此需要它自己的訊號量“limit”,在迴圈期間使用do_with()保活。
  2. 在這裡,我們在繼續迴圈之前不等待slow()完成,即,我們沒有returnfuturize_invoke(slow)開始的future鏈。當訊號量單元可用時,迴圈繼續到下一次迭代,而(在我們的示例中)99 個其他操作可能正在後臺進行,我們不等待它們。

在本節的示例中,我們不能使用with_semaphore()快捷方式。with_semaphore()返回一個僅在 lambda 返回的future解決後才解決的future。但是在上面的例子中,迴圈需要知道何時只有訊號量單元可用,才能開始下一次迭代——而不是等待上一次迭代完成。我們無法通過with_semaphore()來實現. 但是在這種情況下可以使用更通用的異常安全慣用語seastar::get_units(),也是推薦使用的:

seastar::future<> f() {
	return seastar::do_with(seastar::semaphore(100), [] (auto& limit) {
		return seastar::repeat([&limit] {
			return seastar::get_units(limit, 1).then([] (auto units) {
				slow().finally([units = std::move(units)] {});
				return seastar::stop_iteration::no;
			});
		});
	});
}	

上面的例子是不現實的,因為它們有一個永遠不會結束的迴圈,f()返回的future永遠不會解決。在更現實的情況下,迴圈有一個結束,在迴圈結束時,我們需要等待迴圈開始的所有後臺操作。我們可以通過wait()訊號量的原始計數來做到這一點:當完整的計數最終可用時,這意味著所有操作都已完成。例如,以下迴圈在 456 次迭代後結束:

seastar::future<> f() {
	return seastar::do_with(seastar::semaphore(100), [] (auto& limit) {
		return seastar::do_for_each(boost::counting_iterator<int>(0),
				boost::counting_iterator<int>(456), [&limit] (int i) {
			return seastar::get_units(limit, 1).then([] (auto units) {
				slow().finally([units = std::move(units)] {});
			});
		}).finally([&limit] {
			return limit.wait(100);
		});
	});
}

最後一個finally是確保我們等待最後一個操作完成的原因:在repeat迴圈結束後(無論是成功還是由於其中一次迭代中的異常而提前結束),我們執行 wait(100)以等待訊號量達到其原始值 100,意味著我們開始的所有操作都已完成。如果沒有finally,則f()返回的 future將在迴圈的所有迭代實際完成之前解析(最後 100 次可能仍在執行)。

在我們在上面的例子中看到的成語中,相同的訊號量既用於限制後臺操作的數量,又用於等待所有操作完成。有時,我們希望幾個不同的迴圈使用相同的訊號量來限制它們的總並行度。在這種情況下,我們必須使用單獨的機制來等待由迴圈啟動的後臺操作完成。等待正在進行的操作最方便的方法是使用gate,我們將在後面詳細介紹。一個典型的迴圈示例,其並行性受外部訊號量限制:

thread_local seastar::semaphore limit(100);
seastar::future<> f() {
	return seastar::do_with(seastar::gate(), [] (auto& gate) {
		return seastar::do_for_each(boost::counting_iterator<int>(0),
				boost::counting_iterator<int>(456), [&gate] (int i) {
			return seastar::get_units(limit, 1).then([&gate] (auto units) {
				gate.enter();
				seastar::futurize_invoke(slow).finally([&gate, units = std::move(units)] {
					gate.leave();
				});
			});
		}).finally([&gate] {
			return gate.close();
		});
	});
}

在這段程式碼中,我們使用外部訊號量limit來限制併發操作的數量,但另外有一個特定於這個迴圈的gate來幫助我們等待所有正在進行的操作完成。

管道

Seastar的pipe<T>是一種在兩個fiber之間傳輸資料的機制,一個產生資料,另一個消費資料。它有一個固定大小的緩衝區來確保兩個fiber的平衡執行,因為生產者fiber在寫入完整管道時會阻塞,直到消費者fiber開始執行並從管道中讀取。

pipe<T>類似於Unix 管道,因為它具有讀取端、寫入端和它們之間的固定大小的緩衝區,並且支援獨立關閉任一端(以及使用另一端時的 EOF 或 broken pipe)。pipe<T>物件將管道的讀取端和寫入端作為兩個獨立的物件。這些物件可以移動到兩個不同的fiber中。重要的是,如果其中一個管道末端被銷燬(即,捕獲它的continuation結束),管道的另一端將停止阻塞,因此另一根fiber將不會掛起。

管道的讀寫介面是基於future的阻塞。即,write()read() 方法返回一個future,它在操作完成時實現。管道是單讀單寫的,這意味著在 read() 返回的 future 完成之前,不得再次呼叫 read() (對於 write 也是如此)。注意:管道讀取器和寫入器是可move的,但不可複製。將每一端包裝在一個共享指標中通常很方便,這樣它可以被複制(例如,在需要可複製的 std::function 中使用)或輕鬆捕獲到多個continuation中。

使用gate關閉服務

考慮一個有一些長時間操作slow()的應用程式,許多這樣的操作可能隨時啟動。許多slow()操作甚至可以並行進行。現在,您要關閉此服務,但要確保在此之前完成所有未完成的操作。此外,您不希望slow()在關閉過程中允許新操作開始。

這就是seastar::gate的目的。gate g維護正在進行的操作的內部計數器。我們在進入操作時呼叫g.enter()(即在執行slow()之前),在離開操作時呼叫g.leave()(當呼叫slow()完成時)。該方法g.close()關閉了 gate,這意味著它禁止任何進一步的呼叫g.enter()(這種嘗試將產生異常);此外,當所有現有操作都完成時,g.close()返回一個future。換句話說,當g.close()解析時,我們知道不能再呼叫slow()—— 因為已經開始的呼叫已經完成,而新的呼叫無法開始。

構造

seastar::with_gate(g, [] { return slow(); })

可以用作成語的捷徑

g.enter();
slow().finally([&g] { g.leave(); });

這是使用gate的典型示例:

#include <seastar/core/sleep.hh>
#include <seastar/core/gate.hh>
#include <boost/iterator/counting_iterator.hpp>

seastar::future<> slow(int i) {
	std::cerr << "starting " << i << "\n";
	return seastar::sleep(std::chrono::seconds(10)).then([i] {
		std::cerr << "done " << i << "\n";
	});
}
seastar::future<> f() {
	return seastar::do_with(seastar::gate(), [] (auto& g) {
		return seastar::do_for_each(boost::counting_iterator<int>(1),
				boost::counting_iterator<int>(6),
				[&g] (int i) {
			seastar::with_gate(g, [i] { return slow(i); });
			// wait one second before starting the next iteration
			return seastar::sleep(std::chrono::seconds(1));
		}).then([&g] {
			seastar::sleep(std::chrono::seconds(1)).then([&g] {
				// This will fail, because it will be after the close()
				seastar::with_gate(g, [] { return slow(6); });
			});
			return g.close();
		});
	});
}

在這個例子中,我們有一個需要 10 秒才能完成的函式future<> slow()。我們在迴圈中執行 5 次,在兩次呼叫之間等待 1 秒,並在每個呼叫周圍加上進出大門(使用with_gate)。在第 5 次呼叫之後,雖然所有呼叫仍在進行中(因為每個呼叫需要 10 秒才能完成),但我們關閉gate並等待它退出程式。我們還通過在關閉門後一秒鐘嘗試再次進入門來測試無法在關閉門後開始新呼叫。

該程式的輸出如下所示:

starting 1
starting 2
starting 3
starting 4
starting 5
WARNING: exceptional future ignored of type 'seastar::gate_closed_exception': gate closed
done 1
done 2
done 3
done 4
done 5

在這裡, slow()的呼叫以 1 秒的間隔開始。在 " starting 5" 訊息之後,我們關閉了gate,並且再次嘗試使用它導致了seastar::gate_closed_exception,我們忽略了它,因此出現了這條訊息。此時應用程式等待由g.close()返回的future。 這將在所有slow()呼叫完成後發生:列印“done 5”之後,測試程式立即停止。

正如到目前為止所解釋的,gate可以阻止對操作的新呼叫,並等待任何正在進行的操作完成。但是,這些進行中的操作可能需要很長時間才能完成。通常,長時間操作想知道已請求shut-down,這樣它可以提前停止其工作。一個操作可以通過呼叫gate的方法check()來檢查它的gate是否關閉:如果gate已經關閉,check()方法會丟擲一個異常(與enter()一樣丟擲seastar::gate_closed_exception)。目的是異常將導致呼叫它的操作在此時停止。

在前面的示例程式碼中,我們有一個不間斷的操作slow(),它休眠了 10 秒。讓我們將其替換為 10 個一秒睡眠的迴圈,每秒呼叫g.check()一次:

seastar::future<> slow(int i, seastar::gate &g) {
	std::cerr << "starting " << i << "\n";
	return seastar::do_for_each(boost::counting_iterator<int>(0),
								boost::counting_iterator<int>(10),
			[&g] (int) {
		g.check();
		return seastar::sleep(std::chrono::seconds(1));
	}).finally([i] {
		std::cerr << "done " << i << "\n";
	});
}

現在,gate關閉後僅一秒鐘(列印“開始 5”訊息後),所有slow()操作都通知gate關閉,並停止。正如預期的那樣,異常停止了do_for_each()迴圈,並執行了finally()``continuation,因此我們看到所有五個操作的“done”訊息。

相關文章