Task Groups
TaskGroup coordinates several related tasks as one logical group.
It tracks how many tasks were registered, how many are still pending, and how many finished with success, failure, cancellation, timeout, or rejection.
The recommended include is:
#include <vix/threadpool/threadpool.hpp>Basic idea
A task group is useful when several tasks belong to the same operation. For example: load users, load products, load orders. All three tasks are separate, but logically they belong to one larger operation. TaskGroup gives you one place to track that operation.
Create a group
vix::threadpool::TaskGroup group;A new group is empty:
group.empty(); // true
group.done(); // true
group.total_tasks(); // 0
group.pending_tasks(); // 0Register tasks
Use add_task() to register a task id.
group.add_task(vix::threadpool::TaskId{1});
group.add_task(vix::threadpool::TaskId{2});
group.add_task(vix::threadpool::TaskId{3});Now the group knows that 3 tasks are pending:
group.total_tasks(); // 3
group.pending_tasks(); // 3
group.done(); // falseInvalid task ids are rejected:
group.add_task(vix::threadpool::invalid_task_id); // falseFinish a task
When a task completes, call finish_task().
group.finish_task(
vix::threadpool::TaskStatus::completed,
vix::threadpool::TaskResult::success);This decrements the pending count and updates the result counters.
Complete example
#include <chrono>
#include <iostream>
#include <thread>
#include <vix/threadpool.hpp>
int main()
{
vix::threadpool::TaskGroup group;
group.add_task(vix::threadpool::TaskId{1});
group.add_task(vix::threadpool::TaskId{2});
group.add_task(vix::threadpool::TaskId{3});
std::thread first(
[&group]()
{
std::this_thread::sleep_for(std::chrono::milliseconds{20});
group.finish_task(
vix::threadpool::TaskStatus::completed,
vix::threadpool::TaskResult::success);
});
std::thread second(
[&group]()
{
std::this_thread::sleep_for(std::chrono::milliseconds{40});
group.finish_task(
vix::threadpool::TaskStatus::completed,
vix::threadpool::TaskResult::success);
});
std::thread third(
[&group]()
{
std::this_thread::sleep_for(std::chrono::milliseconds{60});
group.finish_task(
vix::threadpool::TaskStatus::cancelled,
vix::threadpool::TaskResult::cancelled);
});
group.close();
group.wait();
first.join();
second.join();
third.join();
std::cout << "total: " << group.total_tasks() << '\n';
std::cout << "completed: " << group.completed_tasks() << '\n';
std::cout << "cancelled: " << group.cancelled_tasks() << '\n';
std::cout << "has error: " << (group.has_error() ? "yes" : "no") << '\n';
return 0;
}Waiting for all tasks
Use wait() to block until all registered tasks finish.
group.wait();wait() returns when pending_tasks == 0. It does not throw.
Wait and rethrow
Use wait_and_rethrow() when you want to wait for all tasks and then rethrow the first captured exception.
group.finish_task(
vix::threadpool::TaskStatus::failed,
vix::threadpool::TaskResult::failure,
std::make_exception_ptr(std::runtime_error{"task failed"}));
try
{
group.wait_and_rethrow();
}
catch (const std::exception &e)
{
std::cout << e.what() << '\n';
}wait_and_rethrow() waits for all tasks first, then rethrows the first captured exception. This avoids leaving other tasks unfinished.
Closing a group
Use close() to prevent new task registrations.
group.close();After closing, new calls to add_task() are rejected, but existing tasks are not cancelled.
Cancellation
A group owns a shared cancellation source.
group.cancel();
group.cancelled(); // trueYou can also get a token:
vix::threadpool::CancellationToken token = group.cancellation_token();
if (token.cancelled()) { ... }Cancellation is cooperative. TaskGroup does not forcibly stop running C++ code. It only exposes shared cancellation state that tasks or higher-level systems can observe.
Counters
TaskGroup tracks these counters:
group.total_tasks()
group.pending_tasks()
group.completed_tasks()
group.failed_tasks()
group.cancelled_tasks()
group.timed_out_tasks()
group.rejected_tasks()Successful task
group.finish_task(
vix::threadpool::TaskStatus::completed,
vix::threadpool::TaskResult::success);
// increments: completed_tasksFailed task
group.finish_task(
vix::threadpool::TaskStatus::failed,
vix::threadpool::TaskResult::failure,
std::make_exception_ptr(std::runtime_error{"failed"}));
// increments: failed_tasks
// stores first exception if none was stored yetCancelled task
group.finish_task(
vix::threadpool::TaskStatus::cancelled,
vix::threadpool::TaskResult::cancelled);
// increments: cancelled_tasksTimed out task
group.finish_task(
vix::threadpool::TaskStatus::timed_out,
vix::threadpool::TaskResult::timeout);
// increments: timed_out_tasksRejected task
group.finish_task(
vix::threadpool::TaskStatus::rejected,
vix::threadpool::TaskResult::rejected);
// increments: rejected_tasksChecking group result
Use has_failure() when you only care about failed tasks:
if (group.has_failure())
{
// At least one task failed.
}Use has_error() when you care about any non-success outcome (failed, cancelled, timed out, or rejected):
if (group.has_error())
{
// At least one task did not succeed.
}Task ids
A group stores all registered task ids:
std::vector<vix::threadpool::TaskId> ids = group.task_ids();This is useful for diagnostics, logs, tracing, testing, and runtime integrations.
First exception
If a task reports an exception, the group stores the first one:
std::exception_ptr exception = group.first_exception();
if (exception)
{
std::rethrow_exception(exception);
}Most code should use group.wait_and_rethrow() instead.
Thread safety
TaskGroup is thread-safe. Multiple threads may call finish_task() while another thread calls wait(). The group uses an internal mutex and condition variable to protect state and wake waiters.
TaskGroup vs Scope
Use TaskGroup when you need manual coordination:
vix::threadpool::TaskGroup group;
group.add_task(id);
group.finish_task(status, result);
group.wait();Use Scope when you want structured concurrency with automatic future tracking:
vix::threadpool::Scope scope(pool);
scope.spawn(fn);
scope.spawn(fn);
scope.wait();For most application code, Scope is simpler. For lower-level runtime integrations, TaskGroup is more explicit.
Common pattern
vix::threadpool::TaskGroup group;
for (int i = 0; i < 10; ++i)
{
const auto id = static_cast<vix::threadpool::TaskId>(i + 1);
group.add_task(id);
pool.post(
[&group]()
{
try
{
// Work
group.finish_task(
vix::threadpool::TaskStatus::completed,
vix::threadpool::TaskResult::success);
}
catch (...)
{
group.finish_task(
vix::threadpool::TaskStatus::failed,
vix::threadpool::TaskResult::failure,
std::current_exception());
}
});
}
group.close();
group.wait_and_rethrow();This pattern provides: explicit registration, completion tracking, exception capture, a final wait, and aggregate result counters.
Best practices
- Register every task before it can finish:
group.add_task(id); - Always call
finish_task()exactly once for every registered task - Close the group when no more tasks will be added:
group.close(); - Use
wait()when you only need completion - Use
wait_and_rethrow()when exceptions matter - Use
cancel()to request cooperative cancellation for the whole group - Do not use
TaskGroupto forcibly stop running tasks
Simple mental model
TaskGroup
stores task ids
counts pending tasks
records final outcomes
stores first exception
exposes shared cancellation
wakes waiters when pending reaches zeroA task group is not the executor. It does not run tasks by itself. It coordinates tasks that are running somewhere else, usually inside a ThreadPool.