Task-Based Parallelism with packaged_task
A packaged_task is a resource handle: it owns its promise and is indirectly responsible for whatever resources its task may own.
Thinking in Units of Work
Task-based parallelism is a programming paradigm in which work is divided into tasks that are executed concurrently. Instead of thinking only about threads or processors, you think about the units of work themselves: each task is a piece of computation that can run alongside others. This article follows one C++ tool that supports this style, the packaged_task template.
Before reading the mechanics, hold one question in mind: if a task is something you want to hand off and run elsewhere, what does the program need to keep track of while that happens? The rest of this article answers that question by looking at how a packaged_task is declared, what it owns, and why it behaves differently from an ordinary value you might copy.
What a packaged_task Is
A packaged_task is a template that supports task-based parallelism in C++. It wraps a task that is passed in when the object is constructed, so that the task can be handed off and run somewhere other than where it was created.
A packaged_task is more than a passive container. It takes responsibility for two things: the promise associated with the task, and, indirectly, any resources the task itself may hold. Because the object carries that responsibility, it is treated as an owner rather than as a simple value that can be duplicated freely. This is the root of the behavior that surprises many learners, and it is explained in the section on move-only ownership below.
A packaged_task is a resource handle: it owns its promise and is indirectly responsible for whatever resources its task may own.
Notice how the object relates to the task it wraps. The task is a callable supplied from outside, and the packaged_task adds the machinery around it: a promise tied to the task and a responsibility for the resources involved. The task describes the work; the packaged_task is the handle that carries that work and its bookkeeping from one place to another.
Reading the Declaration Pattern
A packaged_task is built in two steps that are easy to confuse. The first step is a template argument: you give the template the type of the task. The second step is a constructor argument: you pass the task itself when you create the object. The general pattern is written as packaged_task<Task_type>(task), with the task type in angle brackets and the task in parentheses.
The diagram shows the two roles that a single declaration carries. The angle brackets describe the shape of the task the object is meant to wrap. The parentheses hand in the specific task. Both pieces of information matter, but they answer different questions, which is why the two parts are read separately in the subsections below.
Splitting a Declaration in Two
Identifying the task type and the constructor argument
Read the declaration std::packaged_task<int(int, int)> task(add); and identify the task type and the constructor argument.
- 1Find the angle brackets
The part <
int(int, int)> is the template argument. It is the task type, the kind of work thispackaged_taskis declared to wrap. - 2Find the parentheses
The part (add) is the constructor argument. It supplies the actual task, here the callable named add.
- 3Keep the roles separate
The angle brackets describe the shape of the work, and the parentheses hand in the specific instance. Neither one alone tells the full story.
Task type: int(int, int). Constructor argument: add.
Move-Only Ownership in Action
Because a packaged_task is a resource handle that owns its promise, it cannot be copied. The source states this plainly and gives the reason: two copies would have to share or duplicate the responsibility for the promise and the task's resources, and a copy is not presented as a valid option. Instead, the object is transferred from one place to another using move() operations.
The before and after states show the central idea. Ownership moves rather than being duplicated. Before the move, the variable task is responsible for the promise and the task's resources. After the move, that responsibility sits with the receiving object, and the original variable no longer carries it. Exactly one owner is responsible at any time.
Mistakes When Declaring and Moving
Common mistake Reading only the angle brackets
What it looks likeSeeing packaged_task<int(int, int)> and assuming the specific task is already identified.
Why it failsThe angle brackets give only the task type. The particular instance is the constructor argument in parentheses.
Do this insteadRead the parentheses as the specific task handed in, and keep it separate from the type.
Common mistake Copying a packaged_task to pass it along
What it looks likeWriting code that makes a second packaged_task from the first so that both can be used.
Why it failsA
packaged_taskcannot be copied, because it is a resource handle that owns its promise and is responsible for the task's resources.Do this insteadTransfer the object with move() so that one owner remains responsible.
Common mistake Treating the packaged_task as a simple value
What it looks likeAssuming the object can be duplicated freely, the way an integer or a plain string can.
Why it failsThe object carries ownership of the promise and, indirectly, of the task's resources, so it is an owner and not a passive value.
Do this insteadThink of the packaged_task as a handle with responsibilities, and plan how that responsibility is handed off.
Where a Task's Result Goes
The source identifies the promise as one of the things a packaged_task owns. The promise is the object tied to the task, and the diagram below traces the path a result takes in the standard model for this pattern: the task runs, its return value is stored through the promise, and the caller reads that value through the future it holds.
Read the diagram as a path rather than as a set of parts. The caller hands the work off, the task produces a value on the worker side, and the value reaches the caller through the promise and the future. The source does not walk through this path in detail, so treat the later steps as the general model that explains why the promise matters, not as a full specification of the mechanism.
Checking Your Reading
Given the declaration std::packaged_task<double(double)> job(square); identify the task type and the constructor argument. Then explain why job cannot simply be copied into a second variable, and what you would do instead.
Hints
- The angle brackets describe the kind of task, and the parentheses supply the specific task.
- A
packaged_taskowns its promise and is responsible for the task's resources, so duplicating it would duplicate that responsibility. - The source shows transfers done with
move()operations.
Key Takeaways
- Task-based parallelism executes work as tasks that run concurrently, and
packaged_taskis a template that supports this style. - A
packaged_taskis declared aspackaged_task<Task_type>(task): the template argument gives the kind of task, and the constructor argument supplies the actual task. - A
packaged_taskis a resource handle that owns its promise and is indirectly responsible for the resources of its task, so it cannot be copied. - Ownership is transferred with
move()operations, leaving exactly one owner responsible at any time. - The promise owned by the
packaged_taskis the link through which a task's result is delivered back toward the caller's future.