std::async and std::future

std::async starts a function and gives back a std::future: a handle that will hold the result. get() waits for the task, returns the value, and rethrows an exception the task threw.

The two threads of lesson 105 share a variable and need a join and a lock to hand a result back. A future replaces both: the task returns the value, and the caller reads it when it is ready. This lesson starts four tasks and reads their four results.

A task and its future

std::async(f) runs f on a new thread and returns a future of its return type. The call returns immediately, and get() blocks until the function has returned and gives the value back.

The program sums a vector in the task and prints 10 from the future. The vector is captured by value, so the task owns its copy and the caller may destroy the original.

The future is move-only, like the unique_ptr of lesson 091, because two handles on one result would make the ownership unclear. A function that returns a future moves it out.

#include <future> #include <iostream> #include <numeric> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4}; auto total = std::async([values] { return std::accumulate(values.begin(), values.end(), 0); }); std::cout << total.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra -pthread first.cpp -o first
./first

10

async or deferred

std::launch::async asks for a thread now; std::launch::deferred asks for the function to run on the first get or wait_for, in the calling thread. The default is the bitwise or of the two, and then the implementation decides.

The program asks for both explicitly and checks with wait_for(0ms) which one it got: a deferred future reports future_status::deferred immediately, an async one does not. The last print reads both results, 1 and 2.

The default is the one to avoid when the task has to run before the next line, because a library is allowed to choose deferred and nothing happens until the get. It is also allowed to choose async, so a program that relies on either is not portable.

#include <chrono> #include <future> #include <iostream> int main() { auto lazy = std::async(std::launch::deferred, [] { return 1; }); auto eager = std::async(std::launch::async, [] { return 2; }); std::cout << (lazy.wait_for(std::chrono::milliseconds(0)) == std::future_status::deferred) << ' '; std::cout << (eager.wait_for(std::chrono::milliseconds(0)) != std::future_status::deferred) << ' '; std::cout << lazy.get() << eager.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra -pthread policy.cpp -o policy
./policy

1 1 12

An exception crosses the future

The task runs on another thread, so an exception thrown inside it cannot leave through the caller's stack. The future stores it and get() rethrows it in the caller's thread.

The program's task throws a runtime_error, and the catch prints its message. The object that crossed the thread boundary is a copy of the thrown exception, so the type has to be copyable, which the standard exceptions are.

A task that throws and whose future is never read loses the exception silently. That is one more reason to read every future, and the reason a program that starts many tasks collects them in a vector and gets them all.

#include <future> #include <iostream> #include <stdexcept> int main() { auto task = std::async([] { throw std::runtime_error{"the task failed"}; }); try { task.get(); } catch (const std::runtime_error& error) { std::cout << "caught: " << error.what() << '\n'; } }

g++ -std=c++23 -Wall -Wextra -pthread exception.cpp -o exception
./exception

caught: the task failed

wait_for and the timeout

wait_for waits at most the given duration and reports why it returned: ready, timeout, or deferred. It is how a caller keeps its own deadline without abandoning the task.

The task sleeps 100 milliseconds and main waits 10, so the first report is a timeout and the get afterwards still collects 42. The task was not cancelled; it was finished a moment later by the get.

wait_until takes a time point instead of a duration, which is the form for a deadline that was fixed earlier. Neither call stops the task: C++ futures have no cancellation, which is why lesson 114 adds stop tokens for threads that have to end early.

#include <chrono> #include <future> #include <iostream> #include <thread> int main() { auto task = std::async(std::launch::async, [] { std::this_thread::sleep_for(std::chrono::milliseconds(100)); return 42; }); if (task.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout) { std::cout << "timeout "; } std::cout << task.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra -pthread timeout.cpp -o timeout
./timeout

timeout 42

One future, one get

get() takes the value out of the shared state and leaves the future without one. A second call does not return the value again; it throws std::future_error with the code future_errc::no_state.

The program reads the result once and then tries again inside a catch, which prints the message. The message is std::future_error: No associated state, and the exception carries a code that a program can test instead of the text.

The fix is to store the value when the first get returns it. A future is a one-shot channel, and code that needs the result twice keeps the result, not the future.

#include <future> #include <iostream> int main() { auto task = std::async([] { return 7; }); std::cout << task.get() << ' '; try { std::cout << task.get(); } catch (const std::future_error& error) { std::cout << error.what(); } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra -pthread twice.cpp -o twice
./twice

7 std::future_error: No associated state

Exercises

  1. Start four tasks that each square a number and print the sum of the four results.
  2. Read the futures in a vector and check that the program still works when the tasks finish out of order.
  3. Change the timeout program to wait 200 milliseconds and print which status wait_for reports.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does std::async return?

    Answer

    A std::future that will hold the result

    The call returns a handle immediately and the function runs on its own thread or on demand.

  2. What does get() do when the task threw an exception?

    Answer

    It rethrows a copy of that exception in the calling thread

    The future carries the exception across the thread boundary and get throws it again.

  3. What happens on the second call to get() on the same future?

    Answer

    It throws std::future_error with no associated state

    The first get moves the result out; the shared state no longer holds one for the future.

  4. What does std::launch::deferred ask for?

    Answer

    The task runs on the first get or wait, in the calling thread

    A deferred task is a lazy call: nothing runs until someone asks for the result.

  5. What does this program print? #include <future> #include <iostream> #include <numeric> #include <vector> int main() { std::vector<int> values{1, 2, 3, 4}; auto total = std::async([values] { return std::accumulate(values.begin(), values.end(), 0); }); std::cout << total.get(); }

    Answer

    10

    The task sums 1 to 4, and the future carries the total.

  6. What does this program print? #include <chrono> #include <future> #include <iostream> int main() { auto lazy = std::async(std::launch::deferred, []{ return 1; }); auto eager = std::async(std::launch::async, []{ return 2; }); std::cout << (lazy.wait_for(std::chrono::milliseconds(0)) == std::future_status::deferred) << ' '; std::cout << (eager.wait_for(std::chrono::milliseconds(0)) != std::future_status::deferred) << ' '; std::cout << lazy.get() << eager.get(); }

    Answer

    1 1 12

    The deferred future reports deferred, the async one does not, and the results are 1 and 2.

  7. What does this program print? #include <chrono> #include <future> #include <iostream> #include <thread> int main() { auto task = std::async(std::launch::async, []{ std::this_thread::sleep_for(std::chrono::milliseconds(100)); return 42; }); if (task.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout) std::cout << "timeout "; std::cout << task.get(); }

    Answer

    timeout 42

    The 10 ms wait expires before the 100 ms task ends, and the get then returns 42.

  8. What is the default launch policy of std::async?

    Answer

    async | deferred, chosen by the implementation

    The implementation may start a thread or defer, which is why a program that needs a thread names async.

  9. What does wait_for report?

    Answer

    Whether the result is ready, the wait timed out, or the task is deferred

    It returns a future_status, and the value or exception is still collected by get.

  10. What happens to a future returned by std::async when it is destroyed without get?

    Answer

    The destructor waits for the task to finish

    A future from async blocks in its destructor until the task completes, so the task is never abandoned.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev