Skip to Content
šŸ‘† We offer 1-on-1 classes as well check now

Futures and Promises

Futures and promises are powerful tools in C++ for managing asynchronous operations and transferring data between threads. They provide a mechanism to obtain the result of a computation that may not be immediately available, enabling efficient concurrent programming. Essentially, a promise provides a way to set a value (or exception) that a corresponding future can then retrieve. This decoupling of value production and consumption is crucial for building responsive and scalable applications.

What are Futures and Promises

At their core, futures and promises are about managing the results of asynchronous operations. A promise is an object that can set a value (or an exception) at some point in the future. A future is an object that provides a way to access that value (or exception) once it becomes available. They form a one-way communication channel between a producer (the thread setting the value via the promise) and a consumer (the thread retrieving the value via the future).

In-depth Explanation:

  • Promises: The std::promise class template provides a way to set a value or exception that will be made available to a corresponding std::future. A promise is typically used in a thread that performs a computation. When the computation is complete, the promise is used to set the result. If an exception occurs during the computation, the promise can be used to set the exception.

  • Futures: The std::future class template provides a way to retrieve the value or exception set by a corresponding std::promise. A future is typically used in a thread that needs the result of a computation performed in another thread. The future provides methods to wait for the result to become available and to retrieve the value or exception. The main methods for accessing the result are get() (which blocks until the result is available) and wait() (which allows you to check if the result is available without blocking indefinitely). wait_for() and wait_until() provide timed waiting options.

  • Asynchronous Operations: Futures and promises are most useful when dealing with asynchronous operations, where a task is started in a separate thread and the result is needed later. This allows the main thread to continue processing without blocking, improving responsiveness.

  • Edge Cases:

    • Multiple calls to set_value or set_exception: A promise can only be used to set a value or exception once. Subsequent calls will result in an exception of type std::future_error with an error code of promise_already_satisfied.
    • Detached threads: If a thread associated with a future detaches (using std::thread::detach), the future might become invalid if the thread terminates before the value is set. This can lead to undefined behavior. It’s generally better to join threads or use RAII techniques to ensure proper resource management.
    • Exceptions: Proper exception handling is crucial. If a function executed in a thread throws an exception that is not caught and set on the promise, std::terminate will be called.
  • Performance Considerations:

    • Overhead: Creating and managing futures and promises introduces some overhead. For very short-lived tasks, the overhead might outweigh the benefits of concurrency.
    • Blocking: Calling future::get() blocks the calling thread until the result is available. Excessive blocking can negate the benefits of concurrency. Use wait_for() or wait_until() to avoid indefinite blocking, or consider using callbacks or other asynchronous mechanisms for even greater responsiveness.
    • Data Transfer: Transferring large amounts of data between threads can be a bottleneck. Consider using shared memory or other techniques to minimize data copying.

Syntax and Usage

  • Creating a Promise:

    std::promise<int> myPromise;

    This creates a promise that will hold an integer value.

  • Getting a Future:

    std::future<int> myFuture = myPromise.get_future();

    This retrieves the future associated with the promise.

  • Setting a Value:

    myPromise.set_value(42);

    This sets the value of the promise to 42.

  • Setting an Exception:

    myPromise.set_exception(std::make_exception_ptr(std::runtime_error("Something went wrong")));

    This sets an exception on the promise.

  • Getting a Value from a Future:

    int result = myFuture.get(); // Blocks until the value is available

    This retrieves the value from the future. If an exception was set on the promise, it will be re-thrown by get().

  • Checking if a Future is Ready:

    std::future_status status = myFuture.wait_for(std::chrono::seconds(1)); if (status == std::future_status::ready) { // Value is available } else if (status == std::future_status::timeout) { // Timed out } else if (status == std::future_status::deferred) { // The task is deferred (usually with std::async) }

Basic Example

#include <iostream> #include <future> #include <thread> #include <chrono> int calculateSum(int a, int b, std::promise<int> sumPromise) { std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate some work try { if (a < 0 || b < 0) { throw std::runtime_error("Negative input not allowed"); } sumPromise.set_value(a + b); // Set the result } catch (...) { sumPromise.set_exception(std::current_exception()); // Set the exception } return 0; } int main() { std::promise<int> sumPromise; std::future<int> sumFuture = sumPromise.get_future(); std::thread calculationThread(calculateSum, 10, 20, std::move(sumPromise)); try { int sum = sumFuture.get(); // Blocks until the result is available std::cout << "Sum: " << sum << std::endl; } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; } calculationThread.join(); return 0; }

Explanation:

  1. A promise and its corresponding future are created to hold the result of the sum calculation.
  2. A thread is launched, executing the calculateSum function. std::move is used to transfer ownership of the promise to the thread function.
  3. The calculateSum function simulates some work using std::this_thread::sleep_for.
  4. The calculateSum function checks for invalid input (negative numbers). If invalid input is detected, it sets an exception on the promise. Otherwise, it calculates the sum and sets the value on the promise.
  5. The main function calls sumFuture.get() to retrieve the result. This call blocks until the value is available or an exception is thrown.
  6. The main function handles any exceptions that might be thrown by the calculateSum function.
  7. The main thread joins the calculation thread, ensuring that the thread completes before the program exits.

Advanced Example

#include <iostream> #include <future> #include <thread> #include <vector> #include <numeric> #include <algorithm> template <typename T> std::future<T> parallelAccumulate(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator end, T init) { std::promise<T> resultPromise; std::future<T> resultFuture = resultPromise.get_future(); std::thread([begin, end, init, promise = std::move(resultPromise)]() mutable { try { T result = std::accumulate(begin, end, init); promise.set_value(result); } catch (...) { promise.set_exception(std::current_exception()); } }).detach(); // Note: detach() is used here, be cautious about lifetime return resultFuture; } int main() { std::vector<int> data(10000000); std::iota(data.begin(), data.end(), 1); // Fill with 1, 2, 3, ... // Split the data into two halves auto middle = data.begin() + data.size() / 2; // Calculate the sums in parallel std::future<long long> sum1Future = parallelAccumulate<long long>(data.begin(), middle, 0LL); std::future<long long> sum2Future = parallelAccumulate<long long>(middle, data.end(), 0LL); try { long long sum1 = sum1Future.get(); long long sum2 = sum2Future.get(); std::cout << "Total sum: " << sum1 + sum2 << std::endl; } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; } // Important: Because we used detach(), we have no way to guarantee that the threads // have finished executing before main exits. In a real application, you would // typically use join() or some other synchronization mechanism. This is just // for demonstration purposes. Using detach() without proper resource management // is generally discouraged! A better approach would be to use std::async which // manages the thread lifetime automatically. return 0; }

This example demonstrates parallel accumulation of a large vector using two threads. It splits the vector into two halves and calculates the sum of each half in a separate thread using parallelAccumulate. The results are then combined in the main thread. Note the use of detach(). While convenient, it requires careful consideration of thread lifetimes. It is highly recommended to use std::async instead of manually creating threads and using detach(). std::async automatically manages the thread lifetime and ensures that the result is available before the future is destroyed. This example is simplified for demonstration purposes. In production code, prefer std::async.

Common Use Cases

  • Asynchronous Task Execution: Launching tasks in separate threads and retrieving their results later.
  • Parallel Processing: Dividing a large task into smaller subtasks and executing them in parallel.
  • GUI Applications: Performing long-running operations in a background thread to avoid blocking the UI thread.
  • Network Operations: Handling asynchronous network requests and responses.

Best Practices

  • Use std::async when possible: std::async simplifies asynchronous task execution by automatically managing thread lifetimes and providing a convenient way to retrieve the result.
  • Handle Exceptions Properly: Ensure that exceptions are caught and set on the promise to avoid program termination.
  • Avoid Deadlocks: Be careful when using multiple futures and promises to avoid deadlocks.
  • Use Timed Waits: Use wait_for() or wait_until() to avoid indefinite blocking when waiting for a future.
  • Consider Thread Pools: For managing a large number of asynchronous tasks, consider using a thread pool.

Common Pitfalls

  • Forgetting to Set a Value or Exception: If a promise is destroyed without setting a value or exception, the future will block indefinitely.
  • Multiple Calls to set_value or set_exception: A promise can only be used to set a value or exception once.
  • Deadlocks: Circular dependencies between futures can lead to deadlocks.
  • Ignoring Exceptions: Not handling exceptions thrown by the task associated with a future can lead to program termination.
  • Detached Threads without proper synchronization: Using detach() without a clear understanding of thread lifetimes can lead to unpredictable behavior.

Key Takeaways

  • Futures and promises provide a mechanism for managing asynchronous operations and transferring data between threads.
  • std::promise is used to set a value or exception, and std::future is used to retrieve it.
  • Proper exception handling and deadlock avoidance are crucial when using futures and promises.
  • std::async simplifies asynchronous task execution and should be preferred over manual thread management when possible.
Last updated on