Launching Tasks in C++
The packaged_task connects the work to the result. It is the piece that makes the handle meaningful, because the handle is the only place the code that launched the work needs to look to receive the outcome.
Two Questions Behind Background Work
When a program runs work in the background, two questions come up right away. The first is how the work gets started. The second is how the result of that work gets back to the code that needs it. These two questions are easy to answer for an ordinary function call, and seeing why makes the rest of this article much clearer.
What do you think happens?
In an ordinary function call, how does the caller get the function's result?
Reveal answer
The caller waits while the function runs and receives the return value directly.
A normal call answers both questions at once: it starts the work and delivers the result in the same step. A task that runs separately does not give you that shortcut, so something has to be set up to connect the launch to the returned result.
This article looks at two C++ tools that address this gap. The first is packaged_task, which helps launch tasks and connects up the mechanisms for returning a result. The second is async(), which launches a task in a manner very similar to calling a function. As you read, keep one question in mind: at each step, where is the work, and where is the result?
How packaged_task Links a Task to Its Result
The key word in the definition of packaged_task is connects. A packaged_task is not only a way to start work. It also sets up the path along which the outcome of that work travels back to the code that asked for it. The task and the returned result are linked through the packaged_task, so the code that launches the work and the code that eventually uses the result do not have to share the work itself.
Three pieces are involved. There is the callable, which is the work to be performed. There is the packaged_task, which wraps that callable and serves as the point where launch and connection happen. And there is the handle, the object from which the result can later be read. The diagram below shows how these three pieces relate to one another and to the returned value.
Read the diagram from the top down. The packaged_task holds the callable, and the callable is what eventually produces the value. The value does not sit in the packaged_task. It is delivered to the handle, which is the object the requesting code keeps. This is why the handle is the answer to the question of where the result is: the work and the result are kept apart, and the handle is the link between them.
The packaged_task connects the work to the result. It is the piece that makes the handle meaningful, because the handle is the only place the code that launched the work needs to look to receive the outcome.
State Before and After a packaged_task Launch
The source emphasizes that creating a task and launching a task are different steps. With packaged_task, you build the task explicitly, obtain the handle for its result, and decide when to launch it. The handle is created before the work is launched. The path for the answer is set up first, and the launch step is what fills it.
Before the launch, the packaged_task exists as a prepared unit, and the handle already exists too. Nothing has produced a value yet, so the handle has nothing to hand over. After the launch, the same handle is still the object the caller holds, but now it is connected to work that is underway. The handle is the thing you keep; the value will eventually arrive through it.
How async() Mirrors a Function Call
The important phrase for async() is very similar to calling a function. With async(), you describe the work in the same shape you would use for an ordinary call: a callable followed by its arguments. The difference from a plain call is that the work is launched as a task rather than run inline, and you receive a handle through which the result can later be obtained.
Launching a square computation with async()
Suppose a square function computes the square of its argument, and you want to run it as a task with the argument 7. What does the code that launches it hold afterward?
- 1Describe the work
You provide the callable, which is the square function, and its argument, which is 7. This is the same shape as an ordinary call.
- 2Make one call
A single call to
async()both launches the task and hands back a handle. There is no separate wrapping step and no separate launch step to write. - 3Hold the handle
The calling code now holds a handle, not the number 49 itself. The result is retrieved from that handle later.
- 4Read the result later
When the code needs the outcome, it reads the value through the handle. The value of the square, 49, is what the task produces.
The caller holds a handle to a task that computes 49, and the state to track is small: one call, one handle.
The source notes that the state you should track is much smaller with async(), because the call itself carries both the launch and the handle. You provide the callable and the argument, and you receive a handle to retrieve later. The program does not need a separate wrapping step or a separate launch step.
Calling async() looks like calling a function, but the result comes back as a handle rather than as a direct return value. The work runs as a task, and the handle is how the answer is retrieved.
Comparing Control Over Launch and Result
Both mechanisms deal with the same underlying need, which is running a task and getting its result back. They differ in where the control sits. The diagram below places the two side by side so you can see which steps each one folds together.
| Aspect | packaged_task | async() |
|---|---|---|
| Launch step | Kept separate from building the task | Folded into the same call that returns the handle |
| Handle | Obtained from the task before launching it | Returned by the call that launches the task |
| Shape of code | Build, get handle, then launch | Looks much like an ordinary function call |
| Where control sits | With the programmer, who chooses when to launch | With the single call that launches and returns the handle |
| Tracked state | Task, handle, and a later launch step | Callable, arguments, and one returned handle |
Contrasting the two launch mechanisms, based on the course source
Choosing Between the Two Mechanisms
The choice depends on how much control you need over the launch step. Use the following guide when deciding.
- Choose
packaged_taskwhen you need to build the task explicitly, obtain its handle, and decide yourself when the launch happens. - Choose
async()when you want the launch to look like an ordinary function call, with a callable and its arguments passed in one step and a handle returned for the result. - Choose
async()when the requirement is a simple computation whose result you will collect later, since the call carries both the launch and the handle. - Choose
packaged_taskwhen the connection between launch and result needs to be set up first and filled in later, because the handle is created before the work is launched.
Mistakes Learners Make with Launched Tasks
Common mistake Treating async() as a plain function call that returns the value directly
What it looks likeExpecting the call that uses the square function with 7 to produce the number 49 right away.
Why it failsThe source says
async()launches a task in a manner very similar to calling a function, but the result is retrieved through the handle that the call provides.Do this insteadKeep the handle, and read the result from it when the code needs the outcome.
Common mistake Assuming the result is stored inside the packaged_task
What it looks likeLooking for the answer inside the packaged_task object after the task has been launched.
Why it failsThe
packaged_taskconnects the work to the result, and the value reaches the handle that the requesting code holds.Do this insteadRead the outcome from the handle, the object the launching code keeps.
Common mistake Believing the handle appears only after the result is ready
What it looks likeWaiting for the result before deciding what to store as the handle.
Why it failsThe source states the handle is created before the work is launched, which is how the answer path is set up first.
Do this insteadObtain the handle first, then launch the work, so the launch fills a path that already exists.
Common mistake Forgetting that the choice between the two is about control over the launch step, not about whether the result is delivered
Trace the State Yourself
A program builds a packaged_task for a computation and obtains its handle, but it has not yet launched the task. Describe which objects exist at this moment and what the handle is waiting for. Then describe what changes when the task is launched.
Hints
- Recall that the handle is created before the work is launched.
- Think about which piece holds the callable and which piece will eventually hold the outcome.
- After the launch, what is the handle waiting for?
Compare two requirements: one needs the launching code to decide exactly when the work starts, and one needs a simple background computation whose result is collected later. Which mechanism fits each, and what state does each leave you holding?
Hints
- Ask who controls the launch step in each mechanism.
- Ask which mechanism gives you the handle in one call.
Key Takeaways
packaged_tasklaunches tasks and connects the launch to a returned result, so the work and the outcome are linked through thepackaged_taskand its handle.- The handle for a
packaged_taskresult is created before the work is launched, and the launch step is what fills that path. async()launches a task in a manner very similar to calling a function, taking a callable and its arguments in one call and returning a handle.- Choose
packaged_taskwhen you need explicit control over building the task and deciding when it launches; chooseasync()when a single call that looks like a function call is enough.