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:
| Features | std::async | std::packaged_task |
|---|---|---|
| When to run | Automatic (immediate or delayed) | passive (explicit call) |
| create thread | automatic | Manual |
| Ease of use | Simple | Complex |
| control level | low | High |
| Main use | Simple asynchronous operation | work 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:
| Components | Role |
|---|---|
std::promise<T> | Manually set value/exception on future side** (set_value, set_exception) |
std::packaged_task | Executes a callable object once and automatically writes the results to the associated future |
std::async | Convenience 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 ofpackaged_task/async/promise. - Result Required: Receives value or exception with one
future.get(). Considershared_futurefor multiple subscriptions. - Backpressure·Queue Length Limit: If the producer only
submitsbut 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 ofget(). 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 aboverunWithTimeoutis 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 intopromisesat a low level, but for most cases, automatic handling by **packaged_taskwill 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:
- “C++ Concurrency in Action” by Anthony Williams
- “Effective Modern C++” by Scott Meyers
- cppreference.com - std::packaged_task
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.
Related articles
- C++ async & launch |
- C++ shared_future | Share future results across multiple threads
- C++ future and promise |
- C++ Atomic Operations |
- C++ Attributes |