C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
111: Futures, Promises, and Async Tasks
I remember a developer I mentored a few years back who was building a telemetry dashboard. He needed to fetch data from three different remote sensors, each with a varying response time. His first instinct was to spawn three std::thread objects, but he quickly hit a wall: how do you actually get a value back from a thread? He ended up creating a shared std::vector protected by a std::mutex, and spent two days debugging a race condition where the main thread was trying to render the dashboard before the sensors had actually pushed their data into the vector.
He was doing it the hard way. When you just need a value from an asynchronous operation, you don't want to manage the low-level plumbing of threads and mutexes. That's where futures, promises, and async tasks come in. They provide a higher-level abstraction that lets you treat an asynchronous result as a value that simply "isn't here yet."
Offloading Work with std::async and std::future
The simplest way to start is with std::async. Think of it as a way to tell the runtime, "Run this function whenever you can, and give me a ticket I can use to claim the result later." That ticket is the std::future.
Let's look at a scenario where we're aggregating stock prices from different exchanges. Instead of blocking the main thread while waiting for a slow network response, we can fire off several async tasks:
#include <iostream>
#include <future>
#include <string>
#include <vector>
double fetchPrice(std::string exchange) {
// Simulate a slow network call
if (exchange == "NYSE") return 150.25;
if (exchange == "NASDAQ") return 152.10;
return 0.0;
}
int main() {
// We launch these asynchronously.
// std::launch::async ensures they run in a separate thread.
std::future<double> nysePrice = std::async(std::launch::async, fetchPrice, "NYSE");
std::future<double> nasdaqPrice = std::async(std::launch::async, fetchPrice, "NASDAQ");
// The main thread can do other work here...
std::cout < "Processing other dashboard elements..." < std::endl;
// Now we need the values. .get() blocks until the result is ready.
double total = nysePrice.get() + nasdaqPrice.get();
std::cout < "Average Price: " < total / 2 < std::endl;
return 0;
}
One thing to be careful about: if you don't specify std::launch::async, the implementation might choose std::launch::deferred, which means the function won't actually run until you call .get(). That completely defeats the purpose of concurrency, so I usually recommend being explicit about your launch policy.
Manual Control with std::promise
While std::async is great for "fire and forget" functions, sometimes you need more control. Maybe you have a complex event loop or a callback-based API where the result is produced by some other piece of logic entirely. This is where std::promise comes in.
If a std::future is the "ticket" to claim the value, the std::promise is the "counter" where the value is actually handed over. You create a promise, extract a future from it, and pass the promise to the worker thread. The worker thread can then fulfill that promise at any point in its execution.
I find this particularly useful when you're integrating C++ with an external C library that uses callbacks. You can pass the promise into the callback's user-data pointer, and when the C library finally triggers the callback, you set the value in the promise. The rest of your C++ code, which is holding the future, doesn't need to know anything about the messy callback logic; it just calls .get() and waits for the result.
Keep in mind that calling .get() on a future is a one-time deal. Once you've retrieved the value, the future is invalidated. If you need multiple threads to wait for the same result, you'll want to look into std::shared_future, but for most standard task-based parallelism, the basic promise-future pair is all you'll need.
📋 Practical Task
Exercise: Parallel File Checksum Aggregator
You need to build a tool that calculates the "checksum" (for this exercise, a simple sum of all bytes) of multiple files in parallel. Using std::async, implement a program that:
- Defines a function
long long calculateChecksum(std::string filename)that opens a file in binary mode and returns the sum of all its bytes. - Takes a list of filenames (you can hardcode a vector of strings for this).
- Launches a
std::asynctask for each file. - Stores the resulting
std::future<long long>objects in astd::vector. - Iterates through the vector of futures, calls
.get()on each, and aggregates the final total checksum for all files.
Make sure you use std::launch::async to ensure the files are processed concurrently rather than sequentially.
There are no comments for now.