1

I'm writing some test code using Boost.Asio with C++20 coroutines.

  • Working version (manual cleanup, non-RAII)

    The following code works as expected. Cleanup is always called, even when an exception is thrown from test_body().

    #include <iostream>
    #include <boost/asio.hpp>
    
    namespace asio = boost::asio;
    
    asio::awaitable<void> test_setup() {
        std::cout << "setup" << std::endl;
        co_return;
    }
    
    asio::awaitable<void> test_body() {
        std::cout << "body" << std::endl;
        throw 1; // Simulate exception in test code
        co_return;
    }
    
    asio::awaitable<void> test_cleanup() {
        std::cout << "cleanup" << std::endl;
        co_return;
    }
    
    asio::awaitable<void> test_launcher() {
        co_await test_setup();
    
        try {
            co_await test_body();
        }
        catch (...) {
            std::cout << "caught exception" << std::endl;
        }
    
        // Always perform cleanup
        co_await test_cleanup();
        co_return;
    }
    
    int main() {
        asio::io_context ioc;
        asio::co_spawn(
            ioc.get_executor(),
            test_launcher,
            asio::detached
        );
        ioc.run();
    }
    

    Output:

    setup
    body
    caught exception
    cleanup
    

    Runnable Demo (godbolt)

  • RAII-style version (problematic)

    I want to remove the try-catch from test_launcher() and instead handle exceptions using the third argument of co_spawn. Here's what I tried:

    asio::co_spawn(
        ioc.get_executor(),
        test_launcher,
        [](std::exception_ptr ep) {
            if (ep) {
                try {
                    std::rethrow_exception(ep);
                }
                catch (...) {
                    std::cout << "caught exception" << std::endl;
                }
            }
        }
    );
    

    However, this means that test_cleanup() is not executed when test_body() throws an exception. I tried to work around this using a scope guard.

  • Scope guard workaround (but still not ideal)

    I attempted to implement RAII-style cleanup using a shared_ptr with a custom deleter. Since the deleter cannot be co_await-ed, I spawn a detached coroutine from inside the deleter.

    asio::awaitable<void> test_launcher() {
        co_await test_setup();
        auto exe = co_await asio::this_coro::executor;
    
        std::shared_ptr<void> scope_guard{
            nullptr,
            [exe](void*) {
                std::cout << "scope_guard called" << std::endl;
                asio::co_spawn(
                    exe,
                    test_cleanup,  // this is an awaitable function
                    asio::detached // can't use use_awaitable inside non-awaitable context
                );
            }
        };
    
        co_await test_body();  // throws
        co_return;
    }
    

    Output:

    setup
    body
    scope_guard called
    cleanup begin
    caught exception
    cleanup end
    

    Runnable Demo (godbolt)

Problems:

  • The scope guard (shared_ptr deleter) cannot directly co_await, so I had to co_spawn the cleanup instead.
  • Since the spawned cleanup is detached, I have no way to wait for it to finish.
  • If test_cleanup() is long-running, it may still be executing after the main coroutine ends.

Is there a recommended way to perform RAII-style cleanup for awaitable functions in Boost.Asio coroutines?
That is, I'd like cleanup to always run, even when exceptions are thrown, without manually writing try-catch in every coroutine.

3
  • 1
    The key point is that coros cannot be suspended from a catch block (analogous to the observation that destructors shouldn't throw). So your deleter approach seems best in class to me. Commented Apr 25 at 8:01
  • Thanks, I guess I should consign() any objects that need lifetime management to co_spawn(). That would help avoid the unpredictable completion timing issue. Commented Apr 25 at 21:44
  • 1
    Yeah. It mostly works for resources that are fully constructed ahead of the spawn. I was thinking about a templated coro to wrap the behavior (or ideas around returning RAII resources by unique value) Commented Apr 25 at 22:26

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.