教程翻譯自Seastar官方文檔:https://github.com/scylladb/seastar/blob/master/doc/tutorial.md
轉載請注明出處:https://www.cnblogs.com/morningli/p/15958884.html
協程
注意:協程需要 C++20 和支持的編譯器。已知 Clang 10 及更高版本可以工作。
使用 Seastar 編寫高效異步代碼的最簡單方法是使用協程。協程沒有傳統continuation(如下)的大部分陷阱,因此是編寫新代碼的首選方式。
協程是一個返回 aseastar::future
co_await
或者
co_return
關鍵字的函數。協程對其調用者和被調用者是不可見的;它們以任一角色與傳統的 Seastar 代碼集成。如果對 C++ 協程不熟悉,可以參考
A more general introduction to C++ coroutines ;本節重點介紹協程如何與 Seastar 集成。
下面是一個簡單的 Seastar 協程示例:
#include <seastar/core/coroutine.hh>
seastar::future<int> read();
seastar::future<> write(int n);
seastar::future<int> slow_fetch_and_increment() {
auto n = co_await read(); // #1
co_await seastar::sleep(1s); // #2
auto new_n = n + 1; // #3
co_await write(new_n); // #4
co_return n; // #5
}
在#1 中,我們調用read()函數,它返回一個future
。co_await
關鍵字指示 Seastar 檢查返回的future
。如果 future
就緒,則從 future 中提取值 (int) 並分配給n
。如果future
還沒有就緒,協程安排自己在未來就緒時被調用,並將控制權返回給 Seastar。一旦 future
准備就緒,協程就會被喚醒,並從 future
中提取值並分配給n
.
在 #2 中,我們調用seastar::sleep()
並等待返回的 future
就緒,它會在一秒鍾內完成。這表明n
是跨co_await
調用保留的,協程的作者不需要為協程局部變量安排存儲。
第 #3 行演示了加法運算,假定讀者熟悉該運算。
在 #4 中,我們調用了一個返回 seastar::future<>
的函數。在這種情況下,future
沒有任何值,因此不會提取和分配任何值。
第 #5 行演示了返回一個值。整數值用於滿足調用者在調用協程時得到的future<int>
。
協程中的異常
協程自動將異常轉換為future
並返回。
調用co_await foo()
,當foo()
返回一個異常的future
時,會拋出future
攜帶的異常。
類似地,在協程中拋出將導致協程返回異常的future
。
例子:
#include <seastar/core/coroutine.hh>
seastar::future<> function_returning_an_exceptional_future();
seastar::future<> exception_handling() {
try {
co_await function_returning_an_exceptional_future();
} catch (...) {
// exception will be handled here
}
throw 3; // will be captured by coroutine and returned as
// an exceptional future
}
協程中的並發
co_await
運算符允許簡單的順序執行。多個協程可以並行執行,但每個協程一次只有一個未完成的計算。
類模板seastar::coroutine::all
允許協程分成幾個同時執行的子協程(或 Seastar 纖程,見下文),並在它們完成時再次加入。考慮這個例子:
#include <seastar/core/coroutines.hh>
#include <seastar/coroutine/all.hh>
seastar::future<int> read(int key);
seastar::future<int> parallel_sum(int key1, int key2) {
int [a, b] = co_await seastar::coroutine::all(
[&] {
return read(key1);
},
[&] {
return read(key2);
}
);
co_return a + b;
}
在這里,兩個 read()
調用同時啟動。協程會暫停,直到兩個讀取都完成,並且返回的值被分配給a
和b
。如果read(key)
是一個涉及 I/O 的操作,那么並發執行將比我們co_await
單獨調用每個調用更快完成,因為 I/O 可以重疊。
請注意all
,即使某些子計算拋出異常,它也會等待它的所有子計算。如果拋出異常,則將其傳播到調用協程。
分解長時間運行的計算
Seastar 通常用於 I/O,協程通常會啟動 I/O 操作並消耗其結果,中間幾乎沒有計算。但偶爾需要長時間運行的計算,這可能會阻止反應器執行 I/O 和調度其他任務。
協程會在co_await
表達式中自動讓出;但是在計算中我們不做co_await
。我們可以在這種情況下使用seastar::coroutine::maybe_yield
類:
#include <seastar/coroutine/maybe_yield>
seastar::future<int> long_loop(int n) {
float acc = 0;
for (int i = 0; i < n; ++i) {
acc += std::sin(float(i));
// Give the Seastar reactor opportunity to perform I/O or schedule
// other tasks.
co_await seastar::coroutine::maybe_yield();
}
co_return acc;
}
Continuation
捕獲continuation狀態
我們已經看到 Seastar continuation
是 lambdas,傳遞給future
的then()
方法。在我們目前看到的例子中,lambdas 只不過是匿名函數。但是 C++11 的 lambdas 還有一個技巧,這對於 Seastar 中基於future
的異步編程非常重要:lambdas 可以捕獲狀態。考慮以下示例:
#include <seastar/core/sleep.hh>
#include <iostream>
seastar::future<int> incr(int i) {
using namespace std::chrono_literals;
return seastar::sleep(10ms).then([i] { return i + 1; });
}
seastar::future<> f() {
return incr(3).then([] (int val) {
std::cout << "Got " << val << "\n";
});
}
未來的操作incr(i)
需要一些時間才能完成(它需要先睡一會兒……),在這段時間內,它需要保存它正在處理的值i
。在早期的事件驅動編程模型中,程序員需要顯式定義一個對象來保持這種狀態,並管理所有這些對象。使用 C++11 的 lambda,Seastar 中的一切都變得簡單得多:上面示例中的捕獲語法“[i]”意味着 i 的值,因為它在incr()
被調用時存在,被捕獲到 lambda 中。lambda 不僅僅是一個函數 - 它實際上是一個對象, 代碼和數據。本質上,編譯器自動為我們創建了 state 對象,我們不需要定義它,也不需要跟蹤它(它當 continuation
被延遲時與 continuation
一起保存,並在 continuation
運行后自動刪除)。
一個值得理解的實現細節是,當一個 continuation
捕獲狀態並立即運行時,此捕獲不會產生運行時開銷。但是,當 continuation
不能立即運行(因為 future
還沒有就緒)並且需要保存一段時間,需要在堆上為這些數據分配內存,並且需要將 continuation
捕獲的數據復制到那里。這有運行時開銷,但這是不可避免的,並且與線程編程模型中的相關開銷相比非常小(在線程程序中,這種狀態通常駐留在阻塞線程的堆棧中,但堆棧要比我們微小的捕獲狀態大得多,占用大量內存並在這些線程之間的上下文切換上造成大量緩存污染)。
在上面的示例中,我們通過值捕獲i
—— 即,將值的副本i
保存到continuation
中。C++ 有兩個額外的捕獲選項:通過reference
捕獲和通過move
捕獲:
在延續中使用按reference
捕獲通常是錯誤的,並且可能導致嚴重的錯誤。例如,如果在上面的示例中,我們捕獲了對 i
的引用,而不是復制它,
seastar::future<int> incr(int i) {
using namespace std::chrono_literals;
// Oops, the "&" below is wrong:
return seastar::sleep(10ms).then([&i] { return i + 1; });
}
這意味着continuation
將包含 i
的地址,而不是它的值。但是i
是一個堆棧變量,而incr()函數會立即返回,所以當continuation
最終開始運行時,在incr()
返回很久之后,這個地址將包含不相關的內容。
reference捕獲通常是錯誤
規則的一個例外是do_with()成語,我們將在后面介紹。這個習慣用法確保一個對象在continuation
的整個生命周期中都存在,並且使得通過reference
捕獲成為可能,並且非常方便。
在 continuation
中使用move
捕獲也非常有用。通過將一個對象move
到一個continuation
中,我們將這個對象的所有權轉移給continuation
,並且使對象在continuation
結束時很容易被自動刪除。例如,考慮一個使用std::unique_ptr
int do_something(std::unique_ptr<T> obj) {
// do some computation based on the contents of obj, let's say the result is 17
return 17;
// at this point, obj goes out of scope so the compiler delete()s it.
通過以這種方式使用 unique_ptr
,調用者將一個對象傳遞給函數,但告訴它該對象現在是它的專屬職責——當函數處理完該對象時,它會自動刪除它。我們如何在continuation
中使用 unique_ptr
?以下將不起作用:
seastar::future<int> slow_do_something(std::unique_ptr<T> obj) {
using namespace std::chrono_literals;
// The following line won't compile...
return seastar::sleep(10ms).then([obj] () mutable { return do_something(std::move(obj)); });
}
問題是 unique_ptr
不能按值傳遞給延續,因為這需要復制它,這是被禁止的,因為它違反了該指針僅存在一個副本的保證。但是,我們可以將obj``move
到continuation
中:
seastar::future<int> slow_do_something(std::unique_ptr<T> obj) {
using namespace std::chrono_literals;
return seastar::sleep(10ms).then([obj = std::move(obj)] () mutable {
return do_something(std::move(obj));
});
}
這里使用std::move()
引起obj
的 move-assignment
, 用於將對象從外部函數移動到continuation
中。在 C++11 中引入的move
(move
語義)的概念類似於淺拷貝,然后使源拷貝無效(這樣兩個拷貝就不會共存,正如 unique_ptr
所禁止的那樣)。將 obj
移入 continuation
之后,頂層函數就不能再使用它了(這種情況下當然沒問題,因為我們無論如何都要返回)。
我們在這里使用的[obj = ...]
捕獲語法對於 C++14 來說是新的。這就是 Seastar 需要 C++14 且不支持較舊的 C++11 編譯器的主要原因。
這里需要額外的() mutable
語法,因為默認情況下,當 C++ 將一個值(在本例中為 std::move(obj) 的值)捕獲到 lambda 中時,它會將此值設為只讀,因此在此示例中,我們的 lambda 不能再次移動。添加mutable
消除了這種人為的限制。
鏈式continuation
我們已經在上面的 slow() 中看到了鏈接示例。談論從then
返回,並返回一個future
並鏈接更多的then
。
處理異常
continuation
中拋出的異常被系統隱式捕獲並存儲在future
。存儲此類異常的 future
類似於准備好的 future
,因為它可以導致其繼續被啟動,但它不包含值,僅包含異常。
在這樣的future
調用.then()
會跳過continuation
,並將輸入future
(.then()
被調用的對象)的異常轉移到輸出future
(.then()
的返回值)。
此默認處理與正常的異常行為相似——如果在直線代碼中拋出異常,則跳過以下所有行:
line1();
line2(); // throws!
line3(); // skipped
類似於
return line1().then([] {
return line2(); // throws!
}).then([] {
return line3(); // skipped
});
通常,中止當前的操作鏈並返回異常是需要的,但有時需要更細粒度的控制。有幾種處理異常的原語:
.then_wrapped()
:不是將future
攜帶的值傳遞給continuation
,.then_wrapped()
將輸入future
傳遞給continuation
。這個future
保證處於就緒狀態,因此continuation
可以檢查它是否包含值或異常,並采取適當的行動。.finally()
: 類似於 Java 的 finally 塊,.finally()
無論其輸入future
是否帶有異常,都會執行continuation
。finally
延續的結果是它的輸入future
,因此.finally()
可用於在無條件執行的流程中插入代碼,但不會改變流程。
異常 vs. 異常future
異步函數可以通過以下兩種方式之一失敗:它可以通過拋出異常立即失敗,或者它可以返回最終將失敗的future
(解析為異常)。這兩種失敗模式看起來很相似,但在嘗試使用 finally()
、handle_exception()
或 then_wrapped()
處理異常時是不一樣的行為。例如,考慮以下代碼:
#include <seastar/core/future.hh>
#include <iostream>
#include <exception>
class my_exception : public std::exception {
virtual const char* what() const noexcept override { return "my exception"; }
};
seastar::future<> fail() {
return seastar::make_exception_future<>(my_exception());
}
seastar::future<> f() {
return fail().finally([] {
std::cout << "cleaning up\n";
});
}
如預期的那樣,此代碼將打印“cleaning up”消息 - 異步函數fail()
返回解析為失敗的future
,並且finally()
continuation
盡管出現此失敗,但繼續運行。
現在考慮在上面的例子中我們有一個fail()
不同的定義:
seastar::future<> fail() {
throw my_exception();
}
在這里,fail()
不返回失敗的future
。相反,它根本無法返回future
!它拋出的異常會停止整個函數f()
,並且finally()
延續不會附加到future
(從未返回),並且永遠不會運行。現在不打印“cleaning up”消息。
我們建議為了減少此類錯誤的機會,異步函數應始終返回失敗的future
,而不是拋出實際的異常。如果異步函數在返回未來之前調用另一個函數,並且第二個函數可能會拋出,它應該使用try
/catch
來捕獲異常並將其轉換為失敗的future
:
盡管建議異步函數避免拋出異常,但一些異步函數除了返回異常盡管建議異步函數避免拋出異常,但一些異步函數除了返回異常期貨外,還會拋出異常。一個常見的例子是分配內存並在內存不足時拋出
std::bad_alloc
的函數,而不是返回future
。future<> seastar::semaphore::wait()
方法就是這樣一個函數:它返回一個future
,如果信號量broken()
或等待超時,它可能返回異常的future
,但也可能在分配保存等待者列表的內存失敗時拋出異常。因此,除非一個函數——包括異步函數——被顯式標記為“ noexcept”,應用程序應該准備好處理從它拋出的異常。在現代 C++ 中,代碼通常使用 RAII 來保證異常安全,而不是使用try
/catch
。seastar::defer()
是一個基於 RAII 的習慣用法,即使拋出異常也能確保運行一些清理代碼。
Seastar 有一個方便的通用函數 ,futurize_invoke()
,它在這里很有用。futurize_invoke(func, args...)
運行一個可以返回future
值或立即值的函數,並且在這兩種情況下都將結果轉換為future
值。futurize_invoke()
,還像我們上面所做的那樣將函數拋出的立即異常(如果有)轉換為失敗的future
。因此使用futurize_invoke()
,即使fail()拋出異常,我們也可以使上面的示例工作:
seastar::future<> fail() {
throw my_exception();
}
seastar::future<> f() {
return seastar::futurize_invoke(fail).finally([] {
std::cout << "cleaning up\n";
});
}
請注意,如果異常風險存在於continuation
中,則大部分討論將變得毫無意義。考慮以下代碼:
seastar::future<> f() {
return seastar::sleep(1s).then([] {
throw my_exception();
}).finally([] {
std::cout << "cleaning up\n";
});
}
在這里,第一個延續的 lambda 函數確實拋出了一個異常,而不是返回一個失敗的future
。然而,我們沒有和以前一樣的問題,這只是因為異步函數在返回一個有效的future
之前拋出了一個異常。在這里,f()
確實會立即返回一個有效的未來——只有在sleep()
解決之后才能知道失敗。里面的信息finally()
會被打印出來。附加continuation
的方法(例如then()
和finally()
)以相同的方式運行continuation
,因此continuation
函數可能返回立即值,或者在這種情況下,拋出立即異常,並且仍然正常工作。
生命周期管理
異步函數啟動一個操作,該操作可能會在函數返回后很長時間繼續:函數本身幾乎立即返回 future<T>
,但可能需要一段時間才能解決這個future
。
當這樣的異步操作需要對現有對象進行操作,或者使用臨時對象時,我們需要擔心這些對象的生命周期:我們需要確保這些對象在異步函數完成之前不會被銷毀(否則它會嘗試使用釋放的對象並發生故障或崩潰),並確保對象在不再需要時最終被銷毀(否則我們將發生內存泄漏)。Seastar 提供了多種機制來安全有效地讓對象在適當的時間內保持活動狀態。在本節中,我們將探討這些機制,以及何時使用每種機制。
將所有權傳遞給continuation
確保對象在 continuation
運行並隨后被銷毀時處於活動狀態的最直接方法是將其所有權傳遞給 continuation
。當 continuation
擁有該對象時,該對象將一直保留到 continuation
運行,並在不需要 continuation
時立即銷毀(即,它可能已經運行,或者在出現異常和then()``continuation
時跳過)。
我們已經在上面看到,繼續獲取對象所有權的方法是通過捕獲:
seastar::future<> slow_incr(int i) {
return seastar::sleep(10ms).then([i] { return i + 1; });
}
這里continuation
捕獲i
的值。換句話說,continuation
包含i
的拷貝. 當 continuation
運行 10 毫秒后,它可以訪問此值,並且一旦continuation
完成其對象連同其捕獲的i
的拷貝會被銷毀。continuation
擁有i
的拷貝。
像我們在這里所做的那樣按值捕獲 —— 拷貝我們在延續中需要的對象 —— 主要用於非常小的對象,例如前面示例中的整數。其他對象的復制成本很高,有時甚至無法復制。例如,以下不是一個好主意:
seastar::future<> slow_op(std::vector<int> v) {
// this makes another copy of v:
return seastar::sleep(10ms).then([v] { /* do something with v */ });
}
這將是低效的 —— 因為 vector v
可能很長,將被復制保存在continuation
中。在這個例子中,沒有理由復制v —— 它無論如何都是按值傳遞給函數的,並且在將其捕獲到continuation
之后不會再次使用,因為在捕獲之后,函數立即返回並銷毀其副本v
。
對於這種情況,C++14 允許將對move
到continuation
中:
seastar::future<> slow_op(std::vector<int> v) {
// v is not copied again, but instead moved:
return seastar::sleep(10ms).then([v = std::move(v)] { /* do something with v */ });
}
現在,不是將對象復制v到延續中,而是將其移動到延續中。C++11 引入的移動構造函數將向量的數據移動到延續中並清除原始向量。移動是一種快速操作——對於向量來說,它只需要復制一些小字段,例如指向數據的指針。和以前一樣,一旦延續被解除,向量就會被破壞——它的數據數組(在移動操作中被移動)最終被釋放。
在某些情況下,move
對象是不可取的。例如,某些代碼保留對對象或其字段之一的引用,如果移動對象,引用將變為無效。在一些復雜的對象中,甚至移動構造函數也很慢。對於這些情況,C++ 提供了有用的封裝std::unique_ptr<T>
。一個unique_ptr<T>
對象擁有一個在堆上分配的T
類型的對象。當 unique_ptr<T>
被移動時,類型 T 的對象根本沒有被觸及 —— 只是移動了指向它的指針。std::unique_ptr<T>
在捕獲中使用的一個例子是:
seastar::future<> slow_op(std::unique_ptr<T> p) {
return seastar::sleep(10ms).then([p = std::move(p)] { /* do something with *p */ });
}
std::unique_ptr<T>
是將對象的唯一所有權傳遞給函數的標准 C++ 機制:對象一次僅由一段代碼擁有,所有權通過移動unique_ptr
對象來轉移。unique_ptr
不能被復制:如果我們試圖通過值而不是move
來捕獲p
,我們會得到一個編譯錯誤。
保持對調用者的所有權
我們上面描述的技術——給予它需要處理的對象的持續所有權——是強大而安全的。但通常使用起來會變得困難和冗長。當異步操作不僅涉及一個continuation
,而是涉及每個都需要處理同一個對象的continuation
鏈時,我們需要在每個連續延續之間傳遞對象的所有權,這可能會變得不方便。當我們需要將同一個對象傳遞給兩個單獨的異步函數(或continuation
)時,尤其不方便——在我們將對象移入一個之后,需要返回該對象,以便它可以再次移入第二個。例如,
seastar::future<> slow_op(T o) {
return seastar::sleep(10ms).then([o = std::move(o)] {
// first continuation, doing something with o
...
// return o so the next continuation can use it!
return std::move(o);
}).then([](T o) {
// second continuation, doing something with o
...
});
}
之所以會出現這種復雜性,是因為我們希望異步函數和延續獲取它們所操作的對象的所有權。一種更簡單的方法是讓異步函數的調用者繼續成為對象的所有者,並將對該對象的引用傳遞給需要該對象的各種其他異步函數和continuation
。例如:
seastar::future<> slow_op(T& o) { // <-- pass by reference
return seastar::sleep(10ms).then([&o] {// <-- capture by reference
// first continuation, doing something with o
...
}).then([&o]) { // <-- another capture by reference
// second continuation, doing something with o
...
});
}
這種方法提出了一個問題: slow_op
的調用者現在負責保持對象o
處於活動狀態,而由 slow_op
啟動的異步代碼需要這個對象。但是這個調用者如何知道它啟動的異步操作實際需要這個對象多長時間呢?
最合理的答案是異步函數可能需要訪問它的參數,直到它返回的future
被解析——此時異步代碼完成並且不再需要訪問它的參數。因此,我們建議 Seastar 代碼采用以下約定:
每當異步函數通過引用獲取參數時,調用者必須確保被引用的對象存在,直到函數返回的
future
被解析。
請注意,這只是 Seastar 建議的約定,不幸的是,C++ 語言中沒有強制執行它。非 Seastar 程序中的 C++ 程序員經常將大對象作為 const 引用傳遞給函數,只是為了避免慢速復制,並假設被調用的函數不會在任何地方保存此引用。但在 Seastar 代碼中,這是一種危險的做法,因為即使異步函數不打算將引用保存在任何地方,它也可能會通過將此引用傳遞給另一個函數並最終在延續中捕獲它來隱式地執行此操作。
如果未來的 C++ 版本可以幫助我們發現引用的不正確使用,那就太好了。也許我們可以為一種特殊的引用設置一個標簽,一個函數可以立即使用的“立即引用”(即,在返回未來之前),但不能被捕獲到延續中。
有了這個約定,就很容易編寫復雜的異步函數函數,比如slow_op
通過引用傳遞對象,直到異步操作完成。但是調用者如何確保對象在返回的未來被解決之前一直存在?以下是錯誤的:
seastar::future<> f() {
T obj; // wrong! will be destroyed too soon!
return slow_op(obj);
}
這是錯誤的,因為這里的對象obj
是調用f
的本地對象,並且在f
返回future
時立即銷毀—— 而不是在解決此返回的future
時!調用者要做的正確事情是在堆上創建obj
對象(因此它不會在f
返回時立即被銷毀),然后運行slow_op(obj)
,當future
解決(即使用.finally()
)時,銷毀對象。
Seastar 提供了一個方便的習慣用法,do_with()
用於正確執行此操作:
seastar::future<> f() {
return seastar::do_with(T(), [] (auto& obj) {
// obj is passed by reference to slow_op, and this is fine:
return slow_op(obj);
}
}
do_with
將使用給定的對象執行給定的功能。
do_with
將給定的對象保存在堆上,並使用對新對象的引用調用給定的 lambda。最后,它確保在返回的未來解決后新對象被銷毀。通常, do_with
被賦予一個rvalue
,即一個未命名的臨時對象或一個std::move()
對象,do_with
將該對象移動到它在堆上的最終位置。do_with
返回一個在完成上述所有操作后解析的future
(lambda 的future
被解析並且對象被銷毀)。
為方便起見,do_with
也可以賦予多個對象來保持存活。例如在這里我們創建兩個對象並保持它們直到未來解決:
seastar::future<> f() {
return seastar::do_with(T1(), T2(), [] (auto& obj1, auto& obj2) {
return slow_op(obj1, obj2);
}
}
雖然do_with
打包了它擁有的對象的生命周期,但如果用戶不小心復制了這些對象,這些副本可能具有錯誤的生命周期。不幸的是,像忘記“&”這樣的簡單錯字可能會導致此類意外復制。例如,以下代碼被破壞:
seastar::future<> f() {
return seastar::do_with(T(), [] (T obj) { // WRONG: should be T&, not T
return slow_op(obj);
}
}
在這個錯誤的代碼片段中,obj
不是對do_with
分配對象的引用,而是它的副本 —— 一個在 lambda 函數返回時被銷毀的副本,而不是在它返回的future
解決時。這樣的代碼很可能會崩潰,因為對象在被釋放后被使用。不幸的是,編譯器不會警告此類錯誤。用戶應該習慣於總是使用“auto&”類型do_with
——如上面正確的例子——以減少發生此類錯誤的機會。
同理,下面的代碼片段也是錯誤的:
seastar::future<> slow_op(T obj); // WRONG: should be T&, not T
seastar::future<> f() {
return seastar::do_with(T(), [] (auto& obj) {
return slow_op(obj);
}
}
在這里,雖然obj
被正確的通過引用傳遞給了lambda,但是我們后來不小心傳遞給slow_op()
它的一個副本(因為這里slow_op
是通過值而不是通過引用來獲取對象的),並且這個副本會在slow_op
返回時立即銷毀,而不是等到返回未來解決。
使用 do_with
時,請始終記住它需要遵守上述約定:我們在do_with
內部調用的異步函數不能在返回的future
解析后使用do_with
所持有的對象。這是一個嚴重的use-after-free
錯誤:異步函數返回一個future
,同時仍然使用do_with()
的對象進行后台操作。
通常,在保留后台操作的同時解決異步函數並不是一個好主意——即使這些操作不使用do_with()
的 對象。我們不等待的后台操作可能會導致我們內存不足(如果我們不限制它們的數量),並且很難干凈地關閉應用程序。
共享所有權(引用計數)
在本章的開頭,我們已經注意到將對象的副本捕獲到continuation
中是確保對象在continuation
運行時處於活動狀態並隨后被銷毀的最簡單方法。但是,復雜對象的復制通常很昂貴(時間和內存)。有些對象根本無法復制,或者是讀寫的,延續應該修改原始對象,而不是新副本。所有這些問題的解決方案都是引用計數,也就是共享對象:
Seastar 中引用計數對象的一個簡單示例是seastar::file
,該對象包含一個打開的文件對象(我們將seastar::file
在后面的部分中介紹)。file
對象可以被復制,但復制不涉及復制文件描述符(更不用說文件)。相反,兩個副本都指向同一個打開的文件,並且引用計數增加 1。當文件對象被銷毀時,文件的引用計數減少 1,只有當引用計數達到 0 時,底層文件才真正關閉.
file
對象可以非常快速地復制,並且所有副本實際上都指向同一個文件,這使得將它們傳遞給異步代碼非常方便;例如,
seastar::future<uint64_t> slow_size(file f) {
return seastar::sleep(10ms).then([f] {
return f.size();
});
}
請注意,調用slow_size
與調用slow_size(f)
一樣簡單,傳遞 f
的副本,無需執行任何特殊操作以確保f
僅在不再需要時才將其銷毀。f
什么也沒有做時,這很自然地發生了。
你可能想知道為什么上面的例子return f.size()
是安全的:它不會啟動f
的異步操作嗎(文件的大小可能存儲在磁盤上,所以不能立即可用),f
當我們返回時可能會立即銷毀並且沒有任何東西保留f
的副本?如果f
真的是最后一個引用,那確實是一個錯誤,但還有一個錯誤:文件永遠不會關閉。使代碼有效的假設是有另一個f
的引用將用於關閉它。close 成員函數保持該對象的引用計數,因此即使沒有其他任何東西繼續保持它,它也會繼續存在。由於文件對象生成的所有future
在關閉之前都已完成,因此正確性所需要的只是記住始終關閉文件。
引用計數有運行時開銷,但通常很小;重要的是要記住,Seastar 對象始終僅由單個 CPU 使用,因此引用計數遞增和遞減操作不是通常用於引用計數的慢速原子操作,而只是常規的 CPU 本地整數操作。而且,明智地使用std::move()
和編譯器的優化器可以減少引用計數的不必要的來回遞增和遞減的次數。
C++11 提供了一種創建引用計數共享對象的標准方法——使用模板std::shared_ptr<T>
。shared_ptr
可用於將任何類型包裝到像上面的seastar::file
的引用計數共享對象中。但是,標准std::shared_ptr
在設計時考慮了多線程應用程序,因此它對引用計數使用緩慢的原子遞增/遞減操作,我們已經注意到在 Seastar 中是不必要的。出於這個原因,Seastar 提供了它自己的這個模板的單線程實現,seastar::shared_ptr<T>
. 除了不使用原子操作外,它類似於std::shared_ptr<T>
。
此外,Seastar 還提供了一種開銷更低的變體shared_ptr
:seastar::lw_shared_ptr<T>
. shared_ptr
由於需要正確支持多態類型(由一個類創建的共享對象,並通過指向基類的指針訪問),因此全功能變得復雜。shared_ptr
需要向共享對象添加兩個字,並為每個shared_ptr
副本添加兩個字。簡化版lw_shared_ptr
——不支持多態類型——只在對象中添加一個字(引用計數),每個副本只有一個字——就像復制常規指針一樣。出於這個原因,如果可能(不是多態類型),應該首選輕量級seastar::lw_shared_ptr<T>
,否則seastar::shared_ptr<T>
。較慢的std::shared_ptr<T>
絕不應在分片 Seastar 應用程序中使用。
在堆棧上保存對象
如果我們可以像通常在同步代碼中那樣將對象保存在堆棧中,那不是很方便嗎?即,類似:
int i = ...;
seastar::sleep(10ms).get();
return i;
Seastar 允許通過使用帶有自己堆棧的seastar::thread
對象來編寫此類代碼。使用seastar::thread
的完整示例可能如下所示:
seastar::future<> slow_incr(int i) {
return seastar::async([i] {
seastar::sleep(10ms).get();
// We get here after the 10ms of wait, i is still available.
return i + 1;
});
}
我們在 [seastar::thread
] 部分介紹seastar::thread
,seastar::async()
和seastar::future::get()
。