当前位置 : 主页 > 编程语言 > 其它开发 >

【翻译】Seastar 教程(二)

来源:互联网 收集:自由互联 发布时间:2022-05-17
seastar是用于在现代多核机器上编写高效的复杂服务器的C++库,是高性能宽列存储scylla核心所在。这是seastar教程第二部分,共四部分。 教程翻译自Seastar官方文档:https://github.com/scyllad
seastar是用于在现代多核机器上编写高效的复杂服务器的C++库,是高性能宽列存储scylla核心所在。这是seastar教程第二部分,共四部分。

教程翻译自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()函数,它返回一个futureco_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() 调用同时启动。协程会暂停,直到两个读取都完成,并且返回的值被分配给ab。如果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,传递给futurethen()方法。在我们目前看到的例子中,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``movecontinuation中:

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()引起objmove-assignment, 用于将对象从外部函数移动到continuation中。在 C++11 中引入的movemove语义)的概念类似于浅拷贝,然后使源拷贝无效(这样两个拷贝就不会共存,正如 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
});	

通常,中止当前的操作链并返回异常是需要的,但有时需要更细粒度的控制。有几种处理异常的原语:

  1. .then_wrapped():不是将future携带的值传递给continuation.then_wrapped()将输入future传递给continuation。这个future 保证处于就绪状态,因此continuation可以检查它是否包含值或异常,并采取适当的行动。
  2. .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的函数,而不是返回futurefuture<> seastar::semaphore::wait()方法就是这样一个函数:它返回一个future,如果信号量broken()或等待超时,它可能返回异常的future,但也可能在分配保存等待者列表的内存失败时抛出异常。因此,除非一个函数——包括异步函数——被显式标记为“ noexcept”,应用程序应该准备好处理从它抛出的异常。在现代 C++ 中,代码通常使用 RAII 来保证异常安全,而不是使用 try/catchseastar::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 允许将对movecontinuation中:

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_ptrseastar::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()

上一篇:Vue 源码解读(11)—— render helper
下一篇:没有了
网友评论