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

Viewed 338

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";
}
1 Answers

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.

Related