Seastar’s programming model, using futures and continuations, is very powerful and efficient. However, as we’ve already seen in examples above, it is also relatively verbose: Every time that we need to wait before proceeding with a computation, we need to write another continuation. We also need to worry about passing the data between the different continuations (using techniques like those described in the Lifetime management section). Simple flow-control constructs such as loops also become more involved using continuations. For example, consider this simple classical synchronous code:
std::cout << "Hi.\n";
for (int i = 1; i < 4; i++) {
(1);
sleepstd::cout << i << "\n";
}
In Seastar, using futures and continuations, we need to write something like this:
std::cout << "Hi.\n";
return seastar::do_for_each(boost::counting_iterator<int>(1),
boost::counting_iterator<int>(4), [] (int i) {
return seastar::sleep(std::chrono::seconds(1)).then([i] {
std::cout << i << "\n";
});
});
But Seastar also allows, via seastar::thread
, to write
code which looks more like synchronous code. A
seastar::thread
provides an execution environment where
blocking is tolerated; You can issue an asynchronous function, and wait
for it in the same function, rather then establishing a callback to be
called with future<>::then()
:
::thread th([] {
seastarstd::cout << "Hi.\n";
for (int i = 1; i < 4; i++) {
::sleep(std::chrono::seconds(1)).get();
seastarstd::cout << i << "\n";
}
});
A seastar::thread
is not a separate
operating system thread. It still uses continuations, which are
scheduled on Seastar’s single thread (per core). It works as
follows:
The seastar::thread
allocates a 128KB stack, and runs
the given function until the it blocks on the call to a
future’s get()
method. Outside a
seastar::thread
context, get()
may only be
called on a future which is already available. But inside a thread,
calling get()
on a future which is not yet available stops
running the thread function, and schedules a continuation for this
future, which continues to run the thread’s function (on the same saved
stack) when the future becomes available.
Just like normal Seastar continuations, seastar::thread
s
always run on the same core they were launched on. They are also
cooperative: they are never preempted except when
seastar::future::get()
blocks or on explicit calls to
seastar::thread::yield()
.
It is worth reiterating that a seastar::thread
is not a
POSIX thread, and it can only block on Seastar futures, not on blocking
system calls. The above example used seastar::sleep()
, not
the sleep()
system call. The seastar::thread
’s
function can throw and catch exceptions normally. Remember that
get()
will throw an exception if the future resolves with
an exception.
In addition to seastar::future::get()
, we also have
seastar::future::wait()
to wait without fetching
the future’s result. This can sometimes be useful when you want to avoid
throwing an exception when the future failed (as get()
does). For example:
<char> getchar();
futureint try_getchar() noexcept { // run this in seastar::thread context
= get_char();
future fut .wait();
futif (fut.failed()) {
return -1;
} else {
// Here we already know that get() will return immediately,
// and will not throw.
return fut.get();
}
}
After we created a seastar::thread
object, we need wait
until it ends, using its join()
method. We also need to
keep that object alive until join()
completes. A complete
example using seastar::thread
will therefore look like
this:
#include <seastar/core/sleep.hh>
#include <seastar/core/thread.hh>
::future<> f() {
seastar::thread th([] {
seastarstd::cout << "Hi.\n";
for (int i = 1; i < 4; i++) {
::sleep(std::chrono::seconds(1)).get();
seastarstd::cout << i << "\n";
}
});
return do_with(std::move(th), [] (auto& th) {
return th.join();
});
}
The seastar::async()
function provides a convenient
shortcut for creating a seastar::thread
and returning a
future which resolves when the thread completes:
#include <seastar/core/sleep.hh>
#include <seastar/core/thread.hh>
::future<> f() {
seastarreturn seastar::async([] {
std::cout << "Hi.\n";
for (int i = 1; i < 4; i++) {
::sleep(std::chrono::seconds(1)).get();
seastarstd::cout << i << "\n";
}
});
}
seastar::async()
’s lambda may return a value, and
seastar::async()
returns it when it completes. For
example:
::future<seastar::sstring> read_file(sstring file_name) {
seastarreturn seastar::async([file_name] () { // lambda executed in a thread
= seastar::open_file_dma(file_name).get(); // get() call "blocks"
file f auto buf = f.dma_read(0, 512).get(); // "block" again
return seastar::sstring(buf.get(), buf.size());
});
};
While seastar::thread
s and seastar::async()
make programming more convenient, they also add overhead beyond that of
programming directly with continuations. Most notably, each
seastar::thread
requires additional memory for its stack.
It is therefore not a good idea to use a seastar::thread
to
handle a highly concurrent operation. For example, if you need to handle
10,000 concurrent requests, do not use a seastar::thread
to
handle each — use futures and continuations. But if you are writing code
where you know that only a few instances will ever run concurrently,
e.g., a background cleanup operation in your application,
seastar::thread
is a good match.
seastar::thread
is also great for code which doesn’t care
about performance — such as test code.