C++ packaged_task | "Package Task" Guide

C++ packaged_task | "Package Task" Guide

이 글의 핵심

std::packaged_task is a C++11 feature that wraps a function or callable object, allowing you to receive the result as a std::future. Unlike std::async, you can manually control execution timing, making it useful in work queues or thread pools.

What is packaged_task?

std::packaged_task is a C++11 feature that allows you to wrap a function or callable object and receive the result as a std::future. Unlike std::async, you can manually control execution time, making it useful in work queues or thread pools.

#include <future>

std::packaged_task<int(int)> task([](int x) {
    return x * x;
});

std::future<int> future = task.get_future();
task(10);  // execution

int result = future.get();  // 100

Why do you need it?:

  • Execution Control: Decide when to run
  • Task Queue: Store tasks in a queue and run them later.
  • Thread Pool: Distribute work to worker threads
  • Exception propagation: Propagate exception to future
// std::async: execute immediately (or delay)
auto f1 = std::async([] { return 42; });

// packaged_task: Manual execution
std::packaged_task<int()> task([] { return 42; });
auto f2 = task.get_future();
// Run whenever you want
task();

Default use

// Specify function signature
std::packaged_task<int(int, int)> task([](int a, int b) {
    return a + b;
});

auto future = task.get_future();
task(3, 4);  // execution
int result = future.get();  // 7

Practical example

Example 1: Running in a thread

#include <thread>
#include <future>

int compute(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return x * x;
}

int main() {
    std::packaged_task<int(int)> task(compute);
    std::future<int> future = task.get_future();
    
    std::thread t(std::move(task), 10);
    
std::cout << "Calculating..." << std::endl;
    int result = future.get();
std::cout << "Result: " << result << std::endl;
    
    t.join();
}

Example 2: Job queue

#include <queue>
#include <mutex>

class TaskQueue {
    std::queue<std::packaged_task<void()>> tasks;
    std::mutex mtx;
    
public:
    template<typename F>
    auto enqueue(F&& f) -> std::future<decltype(f())> {
        using ReturnType = decltype(f());
        
        std::packaged_task<ReturnType()> task(std::forward<F>(f));
        auto future = task.get_future();
        
        {
            std::lock_guard<std::mutex> lock(mtx);
            tasks.push(std::move(task));
        }
        
        return future;
    }
    
    void process() {
        std::packaged_task<void()> task;
        
        {
            std::lock_guard<std::mutex> lock(mtx);
            if (tasks.empty()) return;
            
            task = std::move(tasks.front());
            tasks.pop();
        }
        
        task();
    }
};

Example 3: Exception handling

std::packaged_task<int()> task([] {
throw std::runtime_error("error");
    return 42;
});

auto future = task.get_future();
task();

try {
    int result = future.get();  // rethrow exception
} catch (const std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
}

Example 4: Reuse

std::packaged_task<int(int)> task([](int x) {
    return x * 2;
});

auto f1 = task.get_future();
task(10);
int r1 = f1.get();  // 20

// ❌ Not reusable
// task(20);  // error

// ✅ Create new
task = std::packaged_task<int(int)>([](int x) {
    return x * 2;
});

async vs packaged_task

// std::async: autorun
auto f1 = std::async([] { return 42; });

// packaged_task: Manual execution
std::packaged_task<int()> task([] { return 42; });
auto f2 = task.get_future();
task();  // explicit execution

Comparison table:

Featuresstd::asyncstd::packaged_task
When to runAutomatic (immediate or delayed)passive (explicit call)
create threadautomaticManual
Ease of useSimpleComplex
control levellowHigh
Main useSimple asynchronous operationwork queue, thread pool

Practical Selection Guide:

int expensiveComputation();  // Assuming it's defined somewhere

// ✅ Use std::async
// - Simple asynchronous operations
// - No need for thread management
auto result = std::async([] {
    return expensiveComputation();
});

// ✅ Use packaged_task
// - Save to task queue
// - Control when to run
// - Thread pool implementation
std::packaged_task<int()> task(expensiveComputation);
taskQueue.push(std::move(task));
// Later the worker thread runs

Frequently occurring problems

Issue 1: Missing execution

std::packaged_task<int()> task([] { return 42; });
auto future = task.get_future();

// ❌ Do not execute task
// int result = future.get();  // wait forever

// ✅Task execution
task();
int result = future.get();

Issue 2: Move-only

std::packaged_task<int()> task([] { return 42; });

// ❌ No copying allowed
// auto task2 = task;

// ✅ Move
auto task2 = std::move(task);

Problem 3: get_future multiple times

std::packaged_task<int()> task([] { return 42; });

auto f1 = task.get_future();
// auto f2 = task.get_future();  // error

// get_future only happens once

Issue 4: Thread movement

std::packaged_task<int()> task([] { return 42; });
auto future = task.get_future();

// ✅ Delivery on the go
std::thread t(std::move(task));
t.join();

int result = future.get();

Practice pattern

Pattern 1: Simple thread pool

#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>

class ThreadPool {
    std::vector<std::thread> workers_;
    std::queue<std::packaged_task<void()>> tasks_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool stop_ = false;
    
public:
    ThreadPool(size_t numThreads) {
        for (size_t i = 0; i < numThreads; ++i) {
            workers_.emplace_back([this]() {
                while (true) {
                    std::packaged_task<void()> task;
                    
                    {
                        std::unique_lock<std::mutex> lock(mtx_);
                        cv_.wait(lock, [this]() { 
                            return stop_ || !tasks_.empty(); 
                        });
                        
                        if (stop_ && tasks_.empty()) return;
                        
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    
                    task();
                }
            });
        }
    }
    
    template<typename F>
    auto submit(F&& f) -> std::future<decltype(f())> {
        using ReturnType = decltype(f());
        
        std::packaged_task<ReturnType()> task(std::forward<F>(f));
        auto future = task.get_future();
        
        {
            std::lock_guard<std::mutex> lock(mtx_);
            tasks_.push(std::move(task));
        }
        
        cv_.notify_one();
        return future;
    }
    
    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
        for (auto& worker : workers_) {
            worker.join();
        }
    }
};

// use
ThreadPool pool(4);
auto f1 = pool.submit([] { return 42; });
auto f2 = pool.submit([] { return 100; });

std::cout << f1.get() + f2.get() << '\n';  // 142

Pattern 2: Timeout operation

template<typename F>
auto runWithTimeout(F&& f, std::chrono::milliseconds timeout) 
    -> std::optional<decltype(f())> {
    
    std::packaged_task<decltype(f())()> task(std::forward<F>(f));
    auto future = task.get_future();
    
    std::thread t(std::move(task));
    t.detach();
    
    if (future.wait_for(timeout) == std::future_status::ready) {
        return future.get();
    }
    
    return std::nullopt;  // time out
}

// use
auto result = runWithTimeout([] {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
}, std::chrono::seconds(1));

if (result) {
std::cout << "Result: " << *result << '\n';
} else {
std::cout << "Timeout\n";
}

Pattern 3: Cancel operation

class CancellableTask {
    std::packaged_task<int()> task_;
    std::atomic<bool> cancelled_{false};
    
public:
    CancellableTask(std::function<int()> f) 
        : task_([this, f]() {
            if (cancelled_) {
                throw std::runtime_error("Cancelled");
            }
            return f();
        }) {}
    
    std::future<int> getFuture() {
        return task_.get_future();
    }
    
    void run() {
        task_();
    }
    
    void cancel() {
        cancelled_ = true;
    }
};

Relationship to std::promise / std::future

There are three main standard configurations for chaining asynchronous results:

ComponentsRole
std::promise<T>Manually set value/exception on future side** (set_value, set_exception)
std::packaged_taskExecutes a callable object once and automatically writes the results to the associated future
std::asyncConvenience API that bundles function execution and threading policy (whether thread pool reuse is non-standard depending on implementation)

packaged_task has shared state internally and returns a future on the consumer side with get_future(). On the other hand, promise is used by the producer to fill in values ​​that are “still being calculated.” In a task queue, if you wrap the “execution body” in packaged_task, the worker only needs to call operator(), shortening the connection code.

Asynchronous task pattern summary

  • fire-and-forget: If you don’t need the result, you can just set the std::thread + join policy and be done with it, but exception propagation is difficult. To raise results/errors to the top, use one of packaged_task/async/promise.
  • Result Required: Receives value or exception with one future.get(). Consider shared_future for multiple subscriptions.
  • Backpressure·Queue Length Limit: If the producer only submits but consumption cannot keep up, memory increases. Design queue caps, blocking queues, or rejection policies together.

Enhancement with practical examples: Error handling strategies

  • future.get(): Exceptions thrown within a task are saved and rethrown at the time of get(). Therefore it is common to have a try/catch on the calling thread.
  • Timeout: Avoid infinite waiting with wait_for / wait_until, and choose logging/retry/cancel flags on failure. The above runWithTimeout is a demo, and in reality, without cancellation cooperation (periodic flag check), the thread may continue to run, so caution is required in production.
  • std::current_exception: Useful for throwing exceptions into promises at a low level, but for most cases, automatic handling by **packaged_task will suffice.
std::packaged_task<int()> task([] {
    if (!validateInput()) {
        throw std::invalid_argument("bad input");
    }
    return compute();
});
auto fut = task.get_future();
std::thread(std::move(task)).detach();

try {
    use(fut.get());
} catch (const std::exception& e) {
    log_error(e.what());
}

FAQ

Q1: What is packaged_task?

A: A class that wraps a function or callable object so that the result can be received as a std::future. You can manually control when it runs.

Q2: What is the difference from std::async?

A:

  • std::async: automatic execution (immediate or delayed), automatic creation of threads.
  • packaged_task: Manual execution, manual creation of threads
// async: simple
auto f = std::async(compute);

// packaged_task: control
std::packaged_task<int()> task(compute);
auto f = task.get_future();
std::thread t(std::move(task));
t.join();

Q3: Can packaged_task be reused?

A: Impossible. Once you run it, you’ll need to create a new one.

std::packaged_task<int()> task([] { return 42; });
task();
// task();  // error

// create new
task = std::packaged_task<int()>([] { return 42; });

Q4: Can packaged_task be copied?

A: Impossible. Only movement is possible.

std::packaged_task<int()> task1([] { return 42; });
// auto task2 = task1;  // error
auto task2 = std::move(task1);  // OK

Q5: When should I use it?

A:

  • Implement task queue
  • Thread pool implementation
  • When you need to directly control execution timing
  • When you need to save your work and run it later

Q6: Can get_future() be called multiple times?

A: Impossible. get_future() can be called only once.

std::packaged_task<int()> task([] { return 42; });
auto f1 = task.get_future();
// auto f2 = task.get_future();  // error

Q7: How are exceptions handled?

A: Exceptions that occur during task execution are stored in future and are rethrown when calling future.get().

std::packaged_task<int()> task([] {
    throw std::runtime_error("Error");
    return 42;
});

auto f = task.get_future();
task();

try {
    f.get();  // rethrow exception
} catch (const std::exception& e) {
    std::cout << e.what() << '\n';
}

Q8: What are packaged_task learning resources?

A:

Related articles: std::future, std::async, std::promise.

One-line summary: packaged_task wraps a function so that it can receive the result as a future, allowing manual execution control.


Good article to read together (internal link)

Here’s another article related to this topic.

  • C++ async & launch | “Asynchronous Execution” Guide
  • C++ shared_future | Share future results across multiple threads
  • C++ future and promise | “Asynchronous” guide

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, packaged_task, future, async, C++11, etc.


  • C++ async & launch |
  • C++ shared_future | Share future results across multiple threads
  • C++ future and promise |
  • C++ Atomic Operations |
  • C++ Attributes |