教程翻译自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::futureco_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
消除了这种人为的限制。
我们已经在上面的 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()
可用于在无条件执行的流程中插入代码,但不会改变流程。
异步函数可以通过以下两种方式之一失败:它可以通过抛出异常立即失败,或者它可以返回最终将失败的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()
。