How is seastar::thread better than a stackless coroutine?

342 views Asked by At

I looked at this question: Is seastar::thread a stackful coroutine?

In a comment to the answer, Botond Dénes wrote:

That said, seastar::thread still has its (niche) uses in the post-coroutine world as it supports things like waiting for futures in destructors and catch clauses, allowing for using RAII and cleaner error handling. This is something that coroutines don't and can't support.

Could someone elaborate on this? What are the cases when 'things like waiting for futures in destructors and catch clauses' are impossible with stackless coroutines, but possible with seastar::thread (and alike)?

And more generally, what are the advantages of seastar::thread over C++20 stackless coroutines? Do all stackful coroutine implementations (e.g. those in Boost) have these same advantages?

1

There are 1 answers

0
Nadav Har'El On

You have probably heard about the RAII (resource acquisition is initialization) idiom in C++, which is very useful for ensuring that resources get released even in case of exceptions. In the classic, synchronous, C++ world, here is an example:

{
    someobject o = get_an_o();
    // At this point, "o" is holding some resource (e.g., an open file)
    call_some_function(o);
    return;
    // "o" is destroyed if call_some_function() throws, or before the return.
    // When it does, the file it was holding is automatically closed.
}   

Now, let's consider asynchronous code using Seastar. Imagine that get_an_o() takes some time to do its work (e.g., open a disk file), and returns a future<someobject>. Well, you might think that you can just use stackless coroutines like this:

    someobject o = co_await get_an_o();
    // At this point, "o" is holding some resource (e.g., an open file)
    call_some_function(o);
    return;

But there's a problem here... Although the someobject constructor could be made asynchronous (by using a factory function, get_an_o() in this example), the someobject destructor is still synchronous... It gets called by C++ when unwinding the stack, and it can't return a future! So if the object's destructor does need to wait (e.g., to flush the file), you can't use RAII.

Using a seastar::thread allows you to still use RAII because the destructor now can wait. This is because in a seastar::thread any code - including a destructor, may use get() on a future. This doesn't return a future from the destructor (which we can't) - instead it just "pauses" the stackful coroutine (switches to other tasks and later returns back to this stack when the future resolves).