Concepts / Task-Based Parallelism with packaged_task

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.

  • Programming

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

Definition

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.

Key point

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.

std::packaged_taskthe template beinginstantiated<int(int, int)>template argument: the tasktypetaskname of the object beingcreated(add)constructor argument: theactual tasktemplate argumentconstructor argument
In the declaration std::packaged_task<int(int, int)> task(add); which part names the task type, and which part supplies the actual task?

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

Worked example

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.

  1. 1
    Find the angle brackets

    The part <int(int, int)> is the template argument. It is the task type, the kind of work this packaged_task is declared to wrap.

  2. 2
    Find the parentheses

    The part (add) is the constructor argument. It supplies the actual task, here the callable named add.

  3. 3
    Keep 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.

Result

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.

taskowns the packaged_tasktaskno longer owns itworker objectowns the packaged_task nowmove()
When a packaged_task is moved into a worker, where does ownership go, and what is left in the original variable?

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 like

    Seeing packaged_task<int(int, int)> and assuming the specific task is already identified.

    Why it fails

    The angle brackets give only the task type. The particular instance is the constructor argument in parentheses.

    Do this instead

    Read 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 like

    Writing code that makes a second packaged_task from the first so that both can be used.

    Why it fails

    A packaged_task cannot be copied, because it is a resource handle that owns its promise and is responsible for the task's resources.

    Do this instead

    Transfer the object with move() so that one owner remains responsible.

  • Common mistake

    Treating the packaged_task as a simple value

    What it looks like

    Assuming the object can be duplicated freely, the way an integer or a plain string can.

    Why it fails

    The 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 instead

    Think 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.

Callerhands off the taskTaskruns on a workerReturn valueproduced by the taskPromiseowned by the packaged_taskFutureheld by the callerResult readcaller obtains the valuepackaged_task runsproducesstored throughdelivered toretrieved by caller
When the task runs on a worker thread, how does its return value travel back to the future that the caller is waiting on?

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

EASY

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_task owns 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_task is a template that supports this style.
  • A packaged_task is declared as packaged_task<Task_type>(task): the template argument gives the kind of task, and the constructor argument supplies the actual task.
  • A packaged_task is 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_task is the link through which a task's result is delivered back toward the caller's future.

Related concepts

More concepts starting with T