본문으로 건너뛰기
Previous
Next
C++ packaged_task | 'Package Task' Guide

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 and allows 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…

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

The following example demonstrates the concept in cpp:

// 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

Here is the compute implementation:

#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

The following example demonstrates the concept in cpp:

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

The following example demonstrates the concept in cpp:

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

The following example demonstrates the concept in cpp:

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

Here is the runWithTimeout implementation:

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::promiseManually 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.

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++, packaged_task, future, async, C++11 등으로 검색하시면 이 글이 도움이 됩니다.