std::async(3)

πŸ“¦ version 2024.07.05, in nixpkgs unstable, opensuse-leap 16.0

πŸ¦‹ std::async πŸ¦‹

ok so real talk, this is the manual page for std::async(3), straight out of the C++ Standard Library β€” and bestie, it's a whole mood. It's giving "I run your function but like... elsewhere". Buckle up, part 1 goes hard. πŸ’…


πŸ’– NAME πŸ’–

std::async β€” yeah that's it, the name is literally std::async. Iconic of it to serve nothing else in the NAME section. periodt. ✨


πŸŽ€ Synopsis πŸŽ€

Declared up in the header <future>, and it's got variants because of course it does:

Defined in header <future>

template< class F, class... Args >
std::future<typename std::result_of<typename std::decay<F>::type(
            typename std::decay<Args>::type...)>::type>
    async( F&& f, Args&&... args );                 (1)   (since C++11) (until C++17)
template< class F, class... Args >
std::future<std::invoke_result_t<std::decay_t<F>,
                                 std::decay_t<Args>...>>
    async( F&& f, Args&&... args );                 (1)   (since C++17) (until C++20)
template< class F, class... Args >
[[nodiscard]]
std::future<std::invoke_result_t<std::decay_t<F>,
                                 std::decay_t<Args>...>>
    async( F&& f, Args&&... args );                 (1)   (since C++20)
template< class F, class... Args >
std::future<typename std::result_of<typename std::decay<F>::type(
            typename std::decay<Args>::type...)>::type>
    async( std::launch policy, F&& f, Args&&... args );   (2)   (since C++11) (until C++17)
template< class F, class... Args >
std::future<std::invoke_result_t<std::decay_t<F>,
                                 std::decay_t<Args>...>>
    async( std::launch policy, F&& f, Args&&... args );   (2)   (since C++17) (until C++20)
template< class F, class... Args >
[[nodiscard]]
std::future<std::invoke_result_t<std::decay_t<F>,
                                 std::decay_t<Args>...>>
    async( std::launch policy, F&& f, Args&&... args );   (2)   (since C++20)

So what's the vibe? The function template std::async runs the function f asynchronously β€” potentially in a separate thread which might be a part of a thread pool, iykyk β€” and returns a std::future that will eventually hold the result of that function call. 🫢

1) Behaves as if (2) is called with policy being std::launch::async | std::launch::deferred.

2) Calls a function f with arguments args according to a specific launch policy policy (see below, bestie, keep reading).


🚨 the "your program is ill-formed" section (until C++20) 🚨

If any of the following conditions is satisfied, the program is ill-formed, no cap:

  • F is not MoveConstructible.
  • Any type in Args is not MoveConstructible.
  • INVOKE(decay-copy(std::forward<F>(f)), decay-copy(std::forward<Args>(args))...) is not a valid expression.

🚨 same energy, but C++20 onwards (since C++20) 🚨

If any of the following is false, the program is ill-formed:

  • std::is_constructible_v<std::decay_t<F>, F>
  • (std::is_constructible_v<std::decay_t<Args>, Args> && ...)
  • std::is_invocable_v<std::decay_t<F>, std::decay_t<Args>...>

And the sync tea β˜•: the call to std::async synchronizes with the call to f, and the completion of f is sequenced before making the shared state ready. Chef's kiss sequencing. πŸ‘‘


🌸 Parameters 🌸

  • f β€” the callable object to call, the main character of this whole operation. πŸ’«
  • args β€” the parameters you're passing along to f.
  • policy β€” a bitmask value, where individual bits control the allowed methods of execution. yes bitmask, yes vibe.

🧚 Return value 🧚

A std::future referring to the shared state created by this call to std::async. That future is your lil ticket to whatever f eventually hands back, whether that's a value or a whole dramatic exception. 🎟️

πŸ”₯ Launch policies πŸ”₯

⚑ Async invocation

If the async flag is set (i.e. (policy & std::launch::async) != 0), then:

std::async calls INVOKE(decay-copy(std::forward<F>(f)),
       decay-copy(std::forward<Args>(args))...) as if in a new thread     (until C++23)
std::async calls std::invoke(auto(std::forward<F>(f)),
            auto(std::forward<Args>(args))...) as if in a new thread      (since C++23)

…of execution represented by a std::thread object. Bestie, it's literally out here spawning a whole thread for you. πŸ’€

The calls of decay-copy are evaluated (until C++23). The values produced by auto are materialized (since C++23) in the current thread. If the function f returns a value or throws an exception, it is stored in the shared state accessible through the std::future that std::async returns to the caller. So nothing gets lost, it's all kept safe in the shared state. 🫢

😴 Deferred invocation

If the deferred flag is set (i.e. (policy & std::launch::deferred) != 0), then std::async stores:

decay-copy(std::forward<F>(f)) and decay-copy(std::forward<Args>(args))...    (until C++23)
auto(std::forward<F>(f)) and auto(std::forward<Args>(args))...                (since C++23)

…in the shared state. And then it's nap time β€” lazy evaluation is performed:

  • The first call to a non-timed wait function on the std::future that std::async returned to the caller will evaluate INVOKE(std::move(g), std::move(xyz)) in the current thread (which does not have to be the thread that originally called std::async), where:
    • g is the stored value of
decay-copy(std::forward<F>(f))     (until C++23)
auto(std::forward<F>(f))           (since C++23)

and

  • xyz is the stored copy of
decay-copy(std::forward<Args>(args))...   (until C++23)
auto(std::forward<Args>(args))...         (since C++23)
  • The result or exception is placed in the shared state associated with the returned std::future and only then it is made ready. All further accesses to the same std::future will return the result immediately. obsessed with the caching honestly. ✨

πŸ™ˆ Other policies

If neither std::launch::async nor std::launch::deferred, nor any implementation-defined policy flag is set in policy, the behavior is undefined. Like, genuinely undefined. don't do it bestie. 😭

🎯 Policy selection

If more than one flag is set, it is implementation-defined which policy is selected. For the default (both the std::launch::async and std::launch::deferred flags are set in policy), the standard recommends (but does not require) utilizing available concurrency, and deferring any additional tasks. so it's a suggestion, not a promise, iykyk. πŸ’…

If the std::launch::async policy is chosen:

  • a call to a waiting function on an asynchronous return object that shares the shared state created by this std::async call blocks until the associated thread has completed, as if joined, or else time out; and
  • the associated thread completion synchronizes-with the successful return from the first function that is waiting on the shared state, or with the return of the last function that releases the shared state, whichever comes first.

πŸ’€ Exceptions πŸ’€

It throws:

  • std::bad_alloc, if the memory for the internal data structures cannot be allocated, or
  • std::system_error with error condition std::errc::resource_unavailable_try_again, if policy == std::launch::async and the implementation is unable to start a new thread.
    • If policy is std::launch::async | std::launch::deferred or has additional bits set, it will fall back to deferred invocation or the implementation-defined policies in this case. so like, it recovers. graceful queen. πŸ™

πŸ¦„ Notes πŸ¦„

The implementation may extend the behavior of the first overload of std::async by enabling additional (implementation-defined) bits in the default launch policy. plot twist but make it legal. πŸ‘€

Examples of implementation-defined launch policies are the sync policy (execute immediately, within the std::async call) and the task policy (similar to std::async, but thread-locals are not cleared). yes king, undocumented but real. ✨

⚠️ ok this next one is the big one bestie, read it twice. If the std::future obtained from std::async is not moved from or bound to a reference, the destructor of the std::future will block at the end of the full expression until the asynchronous operation completes β€” essentially making code such as the following synchronous:
std::async(std::launch::async, []{ f(); }); // temporary's dtor waits for f()
std::async(std::launch::async, []{ g(); }); // does not start until f() completes

Like… the second one literally can't start until the first finishes. that is SO not the async you ordered. πŸ’€

Note that the destructors of std::futures obtained by means other than a call to std::async never block. only std::async's future does that. rude but true.


🌈 Example 🌈

// Run this code

#include <algorithm>
#include <future>
#include <iostream>
#include <mutex>
#include <numeric>
#include <string>
#include <vector>

std::mutex m;

struct X
{
    void foo(int i, const std::string& str)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << str << ' ' << i << '\n';
    }

    void bar(const std::string& str)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << str << '\n';
    }

    int operator()(int i)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << i << '\n';
        return i + 10;
    }
};

template<typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
    auto len = end - beg;
    if (len < 1000)
        return std::accumulate(beg, end, 0);

    RandomIt mid = beg + len / 2;
    auto handle = std::async(std::launch::async,
                             parallel_sum<RandomIt>, mid, end);
    int sum = parallel_sum(beg, mid);
    return sum + handle.get();
}

int main()
{
    std::vector<int> v(10000, 1);
    std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';

    X x;
    // Calls (&x)->foo(42, "Hello") with default policy:
    // may print "Hello 42" concurrently or defer execution
    auto a1 = std::async(&X::foo, &x, 42, "Hello");
    // Calls x.bar("world!") with deferred policy
    // prints "world!" when a2.get() or a2.wait() is called
    auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");
    // Calls X()(43); with async policy
    // prints "43" concurrently
    auto a3 = std::async(std::launch::async, X(), 43);
    a2.wait();                     // prints "world!"
    std::cout << a3.get() << '\n'; // prints "53"
} // if a1 is not done at this point, destructor of a1 prints "Hello 42" here

ok we're not done yet β€” this page keeps going, part 2 has the rest bestie, don't close the tab πŸ‘€πŸ’–

Possible output:

The sum is 10000
43
world!
53
Hello 42

🩹 Defect reports 🩹

Okay so bestie, plot twist: the standard has been edited retroactively 😭 β€” these are the defect reports that got applied backwards to previously published C++ standards. Yes, time travel is real, it's giving main character energy ✨

  • LWG 2021, applied to C++11 β€” as published, the return type was incorrect and the value category of arguments was unclear in the deferred case. It got corrected: the return type is fixed and it's now clarified that rvalues are used. πŸ’…
  • LWG 2078, applied to C++11 β€” the tea was unclear on whether std::system_error may be thrown if the policy specifies launch policies other than std::launch::async. Now it can only be thrown if policy == std::launch::async. no cap 🫢
  • LWG 2100, applied to C++11 β€” timed waiting functions could not timeout when the std::launch::async policy is used. Now that's allowed. iconic behavior honestly ⏰
  • LWG 2120, applied to C++11 β€” the behavior was unclear if no standard or implementation-defined policy is set. Spoiler alert: the behavior is undefined in this case πŸ’€
  • LWG 2752, applied to C++11 β€” std::async might not throw std::bad_alloc if the memory for the internal data structures cannot be allocated. Now it throws. rude but fair πŸ™ˆ
  • LWG 3476, applied to C++20 β€” (the decayed types of) F and the argument types were directly required to be move constructible. These requirements got removed^[1] πŸŽ€
^[1] The move-constructibility is already indirectly required by std::is_constructible_v. iykyk 🧚

πŸ‘€ See also πŸ‘€

  • future β€” waits for a value that is set asynchronously (C++11) (class template) 🌸

okay that's literally the whole page bestie, you ate that πŸ’…βœ¨ go forth and std::async responsibly (and remember: undefined behavior is not a vibe). πŸ¦‹

This lil glow-up was brought to you by cppreference.com, the 2024.06.10 edition, for the page std::async(3) β€” mwah, ily, bye! πŸ«ΆπŸ’–πŸŽ€

πŸ“¦ where this page comes from