Thread Pool
ThreadPool is the main public execution type in vix::threadpool.
It owns the worker runtime, accepts tasks, returns asynchronous results, exposes task handles, and provides lifecycle and observability operations.
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
auto future = pool.submit([](){
return 42;
});
return future.get() == 42 ? 0 : 1;
}The pool starts automatically when it is constructed.
Create a pool
The simplest form uses the default worker count:
vix::threadpool::ThreadPool pool;You can provide an explicit worker count:
vix::threadpool::ThreadPool pool(4);or a complete configuration:
vix::threadpool::ThreadPoolConfig config;
config.thread_count = 4;
config.max_queue_size = 256;
vix::threadpool::ThreadPool pool(config);The configuration is normalized before the runtime is created.
See Configuration for the fields that currently affect the pool.
Automatic startup
Construction starts the pool immediately:
vix::threadpool::ThreadPool pool(4);
if (!pool.running())
{
return 1;
}Applications normally do not need to call start() themselves.
start() returns true only when the pool transitions from the stopped state to the running state:
const bool started = pool.start();Calling start() while the pool is already running returns false.
For ordinary application code, construction is the normal startup path.
Submit fire-and-forget work
Use post() when the caller does not need a result.
vix::threadpool::ThreadPool pool(4);
const bool accepted = pool.post([](){
// Background work.
});post() returns whether the task was accepted by the execution runtime.
A false result means the work was not accepted.
Typical reasons include:
invalid callable
pool no longer accepting ordinary work
selected worker cannot accept the task
bounded worker queue is fullWhen later code depends on the posted work, synchronize explicitly:
vix::threadpool::ThreadPool pool(4);
const bool accepted = pool.post([](){
// Background work.
});
if (!accepted)
{
return 1;
}
pool.wait_idle();post() is intended for work whose result does not need to be returned to the caller.
Submit work with a result
Use submit() when the callable produces a result.
vix::threadpool::ThreadPool pool(4);
auto future = pool.submit([](){
return 21 * 2;
});
const int result = future.get();The return type is determined from the callable.
For example:
auto integer = pool.submit([](){
return 42;
});
auto text = pool.submit([](){
return std::string{"Vix.cpp"};
});Each submission receives its own asynchronous result.
The callable itself executes on a worker thread when the task reaches execution.
Submit void work with a Future
submit() also supports callables that return void.
vix::threadpool::ThreadPool pool(4);
auto future = pool.submit([](){
// Work with no return value.
});
future.get();In this case, the Future<void> represents completion rather than a value.
This is useful when the caller needs to know whether the operation completed or failed even though the callable does not return data.
If no completion result is needed, post() is simpler.
Passing task options
post(), submit(), and handle() accept TaskOptions.
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_priority(
vix::threadpool::TaskPriority::high
);
auto future = pool.submit([](){
return 42;
}, options);Task options can describe:
priority
cancellation
deadline
timeout
worker affinity
after-stop submissionPool configuration describes the runtime. TaskOptions describes one unit of submitted work.
See Tasks and Options.
Submit move-only work
Submitted callables can own move-only state.
#include <memory>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
auto value = std::make_unique<int>(42);
auto future = pool.submit([value = std::move(value)](){
return *value;
});
return future.get() == 42 ? 0 : 1;
}The callable is moved into the task representation and later executed by the selected worker.
This allows tasks to own resources directly instead of requiring all captured state to be copyable.
Exceptions from submit()
Exceptions thrown by a callable submitted with submit() are captured into the corresponding asynchronous result.
vix::threadpool::ThreadPool pool(4);
auto future = pool.submit([]() -> int {
throw std::runtime_error("failure");
});The exception does not escape the worker thread.
It becomes observable when the caller interacts with the Future.
try
{
future.get();
}
catch (const std::runtime_error&)
{
// Handle failure.
}See Futures and Promises and Errors.
Task handles
Use handle() when the caller needs both the asynchronous result and task-level control.
vix::threadpool::ThreadPool pool(4);
auto handle = pool.handle([](){
return 42;
});
const auto id = handle.id();
const int result = handle.get();A TaskHandle contains:
TaskId
Future
CancellationSourceIt can request cancellation:
handle.cancel();Cancellation is cooperative and does not forcibly interrupt arbitrary C++ code already running on a worker.
See Task Handles.
Task identifiers
Each ordinary submission receives a task identifier generated by the pool.
When using handle(), the identifier is available directly:
auto handle = pool.handle([](){
return 42;
});
const auto id = handle.id();The pool also exposes:
const auto id = pool.next_task_id();This reserves the next task identifier without submitting work.
It is mainly useful when a higher-level system needs to know the identifier before constructing the callable that will be submitted.
Submit with a reserved task ID
A pre-reserved identifier can be used with handle_with_id():
vix::threadpool::ThreadPool pool(4);
const auto id = pool.next_task_id();
auto handle = pool.handle_with_id(id, [id](){
return id;
});The returned handle uses the identifier supplied by the caller.
This is useful for systems that need to establish task identity before submission, for example when external state is keyed by the task ID.
Most application code can use handle() directly.
Submission path
A normal submit() follows this conceptual path:
callable
↓
ThreadPool::submit()
↓
Task
↓
Scheduler
↓
selected worker
↓
worker queue
↓
worker thread
↓
callable execution
↓
Future resultThreadPool is the public facade over this runtime.
Applications do not need to interact directly with the scheduler or workers for normal task execution.
See Architecture and Execution Model.
Worker count
The current number of workers is available through:
const std::size_t workers = pool.thread_count();For example:
#include <iostream>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
std::cout << "workers: " << pool.thread_count() << '\n';
return 0;
}For a fixed explicit configuration:
workers: 4The current runtime creates a fixed worker set from the normalized thread_count.
Pending work
pending() returns the total number of tasks currently queued across all workers.
const std::size_t queued = pool.pending();It does not include tasks that have already been removed from their queues and are currently executing.
Conceptually:
pending()
↓
Queue 1 size
+
Queue 2 size
+
...
+
Queue N sizeBecause tasks can begin execution concurrently, the value is a runtime snapshot and may change immediately after it is read.
Check whether the pool is running
Use:
const bool running = pool.running();running() reports whether the pool is currently in its running state and its scheduler is running.
A newly constructed pool normally reports:
trueAfter shutdown completes:
pool.shutdown();
const bool running = pool.running();the result is false.
running() describes lifecycle state. It does not mean that tasks are currently executing.
Check whether the pool is idle
Use:
const bool idle = pool.idle();The pool is idle when it has no pending work and no active worker execution.
Conceptually:
pending tasks == 0
+
active tasks == 0
↓
idleA pool can therefore be:
running and idleat the same time.
For example, immediately after all current work has completed, the worker threads remain alive and ready for new submissions.
Wait until the pool is idle
Use wait_idle() when the caller needs all currently queued and active work to finish.
vix::threadpool::ThreadPool pool(4);
pool.post([](){
// First task.
});
pool.post([](){
// Second task.
});
pool.wait_idle();After wait_idle() returns, the pool has no pending or active work at the observed idle boundary.
The pool remains running and can accept more work:
pool.wait_idle();
auto future = pool.submit([](){
return 42;
});wait_idle() does not shut down the worker threads.
Waiting and shutting down are different
These operations solve different problems.
pool.wait_idle();means:
wait until current work is finished
keep the pool running
allow future submissionswhile:
pool.shutdown();means:
request runtime shutdown
stop accepting ordinary work
stop and join worker threadsDo not use shutdown when the goal is only to establish a synchronization point.
Shutdown
Shutdown can be requested explicitly:
pool.shutdown();The operation is idempotent.
Calling it more than once is safe:
pool.shutdown();
pool.shutdown();The destructor also calls shutdown() automatically.
For simple scoped usage:
int main()
{
vix::threadpool::ThreadPool pool(4);
auto future = pool.submit([](){
return 42;
});
return future.get() == 42 ? 0 : 1;
}an explicit final shutdown call is not required.
Shutdown and queued work
The treatment of queued work depends on drain_on_shutdown.
With the default configuration:
config.drain_on_shutdown = true;workers process queued work before stopping.
With:
config.drain_on_shutdown = false;queued work does not have the same draining guarantee.
Shutdown does not forcibly kill a worker thread while arbitrary C++ code is already executing.
See Lifecycle and Shutdown for the full contract.
Submissions after shutdown
Ordinary work is accepted only while the pool is running.
For example:
vix::threadpool::ThreadPool pool(4);
pool.shutdown();
const bool accepted = pool.post([](){
// Ordinary submission.
});accepted is false.
For submit() and handle(), rejected submission is represented through their asynchronous result rather than by returning a separate boolean.
Task options also contain an advanced allow_after_stop control used during the shutdown window.
See Lifecycle and Shutdown before relying on after-stop submission.
Clear queued tasks
clear() removes tasks that are still queued and have not started execution.
const std::size_t removed = pool.clear();Running tasks are not interrupted.
Conceptually:
queued tasks
↓
clear()
↓
removed
active tasks
↓
continue runningThe returned value is the number of tasks removed from worker queues.
clear() should not be treated as a general replacement for task cancellation. For result-producing work, use the cancellation model when the caller needs an observable cancellation lifecycle.
See Cancellation.
Periodic tasks
ThreadPool can create a PeriodicTask bound to itself:
auto periodic = pool.schedule_every([](){
// Periodic work.
});Creating the object does not start periodic execution automatically.
The returned PeriodicTask owns the scheduling lifecycle while individual callbacks are dispatched through the pool.
Periodic work is covered separately in Periodic Tasks.
Metrics
metrics() returns a current snapshot of pool activity.
const auto metrics = pool.metrics();It includes runtime values such as worker activity and task counts.
For example:
#include <iostream>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
auto future = pool.submit([](){
return 42;
});
future.get();
pool.wait_idle();
const auto metrics = pool.metrics();
std::cout << "workers: " << metrics.worker_count << '\n';
std::cout << "pending: " << metrics.pending_tasks << '\n';
std::cout << "active: " << metrics.active_tasks << '\n';
return 0;
}Metrics are snapshots. They can change while the pool continues running.
Statistics
stats() returns cumulative execution statistics:
const auto stats = pool.stats();Statistics summarize task outcomes accumulated by the runtime.
Use metrics when you need the current state of the pool.
Use statistics when you need accumulated execution information.
Read the configuration
The normalized configuration stored by the pool is available through:
const auto& config = pool.config();For example:
vix::threadpool::ThreadPoolConfig config;
config.thread_count = 4;
config.max_queue_size = 256;
vix::threadpool::ThreadPool pool(config);
const auto& effective = pool.config();The returned reference remains owned by the pool.
See Configuration for which configuration fields currently affect execution.
ThreadPool is not copyable or movable
A ThreadPool owns a scheduler and worker threads.
Copy and move operations are disabled:
copy construction disabled
copy assignment disabled
move construction disabled
move assignment disabledCreate the pool in the location that owns its runtime lifetime.
When another component only needs access to execution, pass a reference or use the executor abstractions described in Executors.
ThreadPool as an Executor
ThreadPool implements Executor.
This means it can be passed to code that only needs fire-and-forget execution:
void dispatch(vix::threadpool::Executor& executor)
{
executor.post([](){
// Work to execute.
});
}
int main()
{
vix::threadpool::ThreadPool pool(4);
dispatch(pool);
pool.wait_idle();
return 0;
}Using the base interface does not create another pool.
The same underlying workers execute the posted work.
Choosing the submission API
The three main submission forms serve different needs.
Use post()
When the caller only needs to know whether the work was accepted:
const bool accepted = pool.post([](){
// Work.
});Use submit()
When the caller needs the eventual result:
auto future = pool.submit([](){
return 42;
});Use handle()
When the caller needs the result together with task identity and cancellation control:
auto handle = pool.handle([](){
return 42;
});The choice can be summarized as:
Need result?
│
├── no → post()
│
└── yes
│
├── result only → submit()
│
└── result + task control → handle()These operations use the same underlying pool runtime.
Typical lifecycle
A common ThreadPool workflow is:
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
auto first = pool.submit([](){
return 20;
});
auto second = pool.submit([](){
return 22;
});
const int result = first.get() + second.get();
pool.wait_idle();
return result == 42 ? 0 : 1;
}Conceptually:
construct pool
↓
workers start
↓
submit work
↓
workers execute tasks
↓
consume results
↓
wait if required
↓
pool leaves scope
↓
shutdown and joinFor most applications, this is the complete lifecycle needed to begin using the module.
Continue with Execution Model for the detailed path a task follows from submission to completion.