Asynchronous Programming with Seastar

Nadav Har’El - nyh@ScyllaDB.com

Avi Kivity - avi@ScyllaDB.com

Back to table of contents. Previous: 25. More about the sharded network stack. Next: 27. Streams.

26 Seastar::thread

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++) {
        sleep(1);
        std::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 asyncrhonous function, and wait for it in the same function, rather then establishing a callback to be called with future<>::then():

    seastar::thread th([] {
        std::cout << "Hi.\n";
        for (int i = 1; i < 4; i++) {
            seastar::sleep(std::chrono::seconds(1)).get();
            std::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::threads 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 explict 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:

    future<char> getchar();
    int try_getchar() noexcept { // run this in seastar::thread context
        future fut = get_char();
        fut.wait();
        if (fut.failed()) {
            return -1;
        } else {
            // Here we already know that get() will return immediately,
            // and will not throw.
            return fut.get();
        }
    }

TODO: talk about task quota and maybe_yield(), and should_yield().

26.1 Starting and ending a seastar::thread

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>
seastar::future<> f() {
    seastar::thread th([] {
        std::cout << "Hi.\n";
        for (int i = 1; i < 4; i++) {
            seastar::sleep(std::chrono::seconds(1)).get();
            std::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>
seastar::future<> f() {
    return seastar::async([] {
        std::cout << "Hi.\n";
        for (int i = 1; i < 4; i++) {
            seastar::sleep(std::chrono::seconds(1)).get();
            std::cout << i << "\n";
        }
    });
}

seastar::async()’s lambda may return a value, and seastar::async() returns it when it completes. For example:

seastar::future<seastar::sstring> read_file(sstring file_name) {
    return seastar::async([file_name] () {  // lambda executed in a thread
        file f = seastar::open_file_dma(file_name).get0();  // get0() call "blocks"
        auto buf = f.dma_read(0, 512).get0();  // "block" again
        return seastar::sstring(buf.get(), buf.size());
    });
};

While seastar::threads 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.

Back to table of contents. Previous: 25. More about the sharded network stack. Next: 27. Streams.