Search code examples
c++boost

Different behavior of boost::future and std::future


To be specific, boost::future doesn't seem to block in the destructor. And boost documentation didn't mention it. However std::future did mention it.

If I want to chain some .then() to boost::future I also need to chain a .get() call at the end to force the temporary object to block to get the correct behavior. Is that the right way of using it?

void a()
{
    boost::async([] {
        boost::this_thread::sleep_for(boost::chrono::seconds{1});
        std::cout << "Finished sleeping\n";
        return 1;
    });
    std::cout << "End of a()\n";
}
void b()
{
    std::async([] {
        std::this_thread::sleep_for(std::chrono::seconds{1});
        std::cout << "Finished sleeping\n";
        return 1;
    });
    std::cout << "End of b()\n";
}
int main()
{
    a();//No finished sleeping printed
    std::cout << "End of main()\n";
}
int main()
{
    b();//finished sleeping will print
    std::cout << "End of main()\n";
}

Solution

  • https://www.boost.org/doc/libs/1_75_0/doc/html/thread/synchronization.html

    The returned futures behave as the ones returned from boost::async, the destructor of the future object returned from then will block. This could be subject to change in future versions.

    (2015) https://www.boost.org/doc/libs/1_75_0/doc/html/thread/changes.html

    Version 4.5.0 - boost 1.58
    Fixed Bugs:
    #10968 The futures returned by async() and future::then() are not blocking.

    (2017) https://github.com/boostorg/thread/issues/194

    I'm aware of the issue. Boost.thread did it by design following std::async and the first proposals of std::future::then.

    The problem is that changing the behavior (even if it is not desired in some contexts) would be a breaking change. I would need to crate a new version, but Idon't want to create a new version using compiler flags as this has become a nightmare to maintain.

    So I will need to create a boost/thread_v5 which will produce different binaries lib_boost_thread_v5.a. Boost.Threads is not structured this way, and this will mean a lot of work.
    This is the single way to avoid breaking changes, but before doing that I would like to enumerate all the breaking changes that should be on this new version.

    It seems the destructor of future object returned from async has really changed to non-blocking at some time point after 2017, so you should use .get() to block.


    I can not make it block because:

    • BOOST_THREAD_FUTURE_BLOCKING(not documented) is not #defined in all cases;
    • if you #define BOOST_THREAD_FUTURE_BLOCKING manually, you can see that ~future_async_shared_state_base() will be called when the future is destroyed and the lambda passed to async has finished running (i.e. when the boost::shared_ptr's use_count drops from 2 to 1 to 0), so the destructor of future object returned from async (when the boost::shared_ptr's use_count drops from 2 to 1) will still not block.

    https://www.boost.org/doc/libs/1_75_0/boost/thread/detail/config.hpp

    #if BOOST_THREAD_VERSION>=5
    //#define BOOST_THREAD_FUTURE_BLOCKING
    
    #if ! defined BOOST_THREAD_PROVIDES_EXECUTORS \
     && ! defined BOOST_THREAD_DONT_PROVIDE_EXECUTORS
    #define BOOST_THREAD_PROVIDES_EXECUTORS
    #endif
    
    #else
    //#define BOOST_THREAD_FUTURE_BLOCKING
    #define BOOST_THREAD_ASYNC_FUTURE_WAITS
    #endif
    

    https://www.boost.org/doc/libs/1_75_0/boost/thread/future.hpp
    in async:

              void init(BOOST_THREAD_FWD_REF(Fp) f)
              {
    #ifdef BOOST_THREAD_FUTURE_BLOCKING
                this->thr_ = boost::thread(&future_async_shared_state::run, static_shared_from_this(this), boost::forward<Fp>(f));
    #else
                boost::thread(&future_async_shared_state::run, static_shared_from_this(this), boost::forward<Fp>(f)).detach();
    #endif
              }
    

    when the future is destroyed and the lambda passed to async has finished running:

              ~future_async_shared_state_base()
              {
    #ifdef BOOST_THREAD_FUTURE_BLOCKING
                join();
    #elif defined BOOST_THREAD_ASYNC_FUTURE_WAITS
                unique_lock<boost::mutex> lk(this->mutex);
                this->waiters.wait(lk, boost::bind(&shared_state_base::is_done, boost::ref(*this)));
    #endif
              }
    

    C++20 has moved to coroutine which is able to split control flow in more complex cases than future.then: CppCon 2015: Gor Nishanov “C++ Coroutines - a negative overhead abstraction". You can try libraries like cppcoro or boost.fiber and pause the task function inside complex control structures (e.g. co_await inside if inside while inside for).

    C++23 Executor TS: in initial versions of A Unified Executors Proposal for C++ there were OneWayExecutor execute(fire-and-forget) TwoWayExecutor twoway_execute(return a future) then_execute(take a future and a function, return a future) etc but in recent versions they were replaced by sender and receiver.