Task Handles
TaskHandle<T> represents a result-producing task together with its task identifier and cancellation control.
Create one with ThreadPool::handle():
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
auto handle = pool.handle([](){
return 42;
});
return handle.get() == 42 ? 0 : 1;
}Use a task handle when a Future alone is not enough and the caller also needs to identify the task or request cancellation.
What a TaskHandle contains
A task handle combines three pieces of state:
TaskHandle<T>
├── TaskId
├── Future<T>
└── CancellationSourceThese parts serve different purposes.
TaskId
↓
identify the submitted task
Future<T>
↓
wait for and retrieve the result
CancellationSource
↓
request cancellation before execution beginsThe handle does not own the thread pool or worker executing the task.
Create a handle
Use handle() instead of submit():
auto handle = pool.handle([](){
return 42;
});The result type is inferred from the callable, just as it is with submit().
For example:
auto integer = pool.handle([](){
return 42;
});
auto text = pool.handle([](){
return std::string{"Vix.cpp"};
});A callable returning void produces a TaskHandle<void>:
auto handle = pool.handle([](){
perform_work();
});
handle.get();For TaskHandle<void>, get() waits for successful completion without returning a value.
TaskHandle vs Future
submit() returns a Future:
auto future = pool.submit([](){
return 42;
});handle() returns a TaskHandle:
auto handle = pool.handle([](){
return 42;
});The difference is:
Future<T>
├── wait
├── result
└── asynchronous state
TaskHandle<T>
├── everything available through its Future
├── TaskId
└── cancellation sourceUse submit() when the result is all the caller needs.
Use handle() when task identity or cancellation control is also required.
Task identity
Every handle created by ThreadPool::handle() receives a TaskId.
auto handle = pool.handle([](){
return 42;
});
const auto id = handle.id();The identifier is assigned by the pool before the task is submitted to the scheduler.
The first automatically generated identifier in a pool starts at 1, because 0 is reserved as:
vix::threadpool::invalid_task_idTask identifiers are generated by each ThreadPool instance.
They should be treated as identifiers for tasks belonging to that pool, not as globally unique process-wide identifiers.
Check handle validity
Use:
if (handle.valid())
{
// The handle contains a valid task ID and Future state.
}The boolean conversion provides the same check:
if (handle)
{
// Valid handle.
}A default-constructed handle is invalid:
vix::threadpool::TaskHandle<int> handle;
if (!handle)
{
// No valid submitted task is represented.
}valid() checks that:
TaskId is valid
+
Future owns shared stateIt does not mean that the scheduler accepted or successfully executed the task.
A rejected submission can still return a valid handle whose Future contains a rejection result.
Use status(), result(), error(), or get() to inspect the outcome.
Wait for completion
Use wait() when the caller needs to wait without consuming the result:
auto handle = pool.handle([](){
return 42;
});
handle.wait();After wait() returns, the asynchronous result is ready.
The value can still be retrieved afterward:
handle.wait();
const int value = handle.get();wait() does not consume the result.
Check readiness
Use:
if (handle.ready())
{
// A value, exception, or threadpool error is available.
}ready() means that the underlying Future has reached a ready state.
A ready handle may represent:
successful value
exception
cancellation
timeout
rejectionReadiness does not imply success.
Retrieve the result
Use get() to wait for and consume the result:
auto handle = pool.handle([](){
return 42;
});
const int value = handle.get();If the result is not ready, get() waits.
For successful non-void work, it returns the stored value.
For successful void work:
auto handle = pool.handle([](){
perform_work();
});
handle.get();it returns after completion.
get() consumes the result
The underlying Future uses single-consumer semantics.
Calling get() more than once throws std::future_error.
auto handle = pool.handle([](){
return 42;
});
const int value = handle.get();
// Calling handle.get() again is invalid.The same rule applies if the underlying future is consumed directly through future().
For example:
auto handle = pool.handle([](){
return 42;
});
const int value = handle.future().get();After this call, handle.get() cannot retrieve the value again.
Exceptions
Exceptions thrown by the callable are stored in the underlying asynchronous state.
auto handle = pool.handle([]() -> int {
throw std::runtime_error("failure");
});The exception is rethrown by get():
try
{
const int value = handle.get();
}
catch (const std::runtime_error&)
{
// Handle task failure.
}The exception does not escape directly from the worker thread.
ThreadPool errors
A handle can also complete with a ThreadPool-specific error.
For example, cancellation or rejected submission can produce a ThreadPoolErrc in the underlying Future.
Calling get() in this case throws std::system_error.
try
{
const int value = handle.get();
}
catch (const std::system_error&)
{
// Handle threadpool error.
}The error can be inspected without consuming the result:
const auto error = handle.error();See Errors for the error model.
Request cancellation
Call:
handle.cancel();to request cancellation through the source owned by the handle.
For example:
vix::threadpool::ThreadPool pool(1);
auto handle = pool.handle([](){
return 42;
});
handle.cancel();The operation is idempotent. Repeated cancellation requests keep the same cancellation state.
handle.cancel();
handle.cancel();Cancellation is cooperative and does not forcibly terminate a running C++ function.
Cancellation before execution
The cancellation source owned by the handle is connected to the submitted task.
Before the user callable begins, the result wrapper checks that cancellation state.
Conceptually:
handle()
↓
task queued
↓
handle.cancel()
↓
worker reaches task
↓
cancellation requested?
│
├── yes → do not call user function
│
└── no → execute user functionThis makes task handles useful for cancelling work that is still waiting to execute.
A typical example uses a busy single-worker pool:
#include <chrono>
#include <thread>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(1);
auto blocker = pool.submit([](){
std::this_thread::sleep_for(std::chrono::milliseconds{100});
});
auto handle = pool.handle([](){
return 42;
});
handle.cancel();
blocker.get();
return handle.cancelled() ? 0 : 1;
}The second task remains queued while the first task occupies the only worker, giving the cancellation request time to become visible before its callable starts.
Cancellation after execution begins
cancel() does not stop arbitrary C++ code that has already begun executing.
The current handle execution path checks its cancellation source immediately before invoking the user callable.
Once the callable has started, requesting cancellation does not interrupt it.
For example:
task starts
↓
user callable begins
↓
handle.cancel()
↓
cancellation request becomes true
↓
callable continues runningThe callable may still return normally and its Future may complete successfully.
This distinction is important:
handle.cancelled()
↓
cancellation was requested
handle.status() == cancelled
↓
the asynchronous result completed as cancelledThese are not the same condition.
See Cancellation for the complete cancellation model.
Check whether cancellation was requested
Use:
const bool requested = handle.cancelled();This queries the cancellation source owned by the handle.
It answers:
Has cancellation been requested?It does not answer:
Did the task finish as cancelled?To inspect the terminal asynchronous result, use:
handle.status();
handle.result();
handle.error();Task status
status() forwards to the underlying Future state:
const auto status = handle.status();Possible terminal values include:
completed
failed
cancelled
timed_out
rejectedBefore the asynchronous state becomes ready, the current Future state reports:
createdThe handle's Future state does not currently mirror the low-level task's intermediate queued and running transitions.
Therefore, do not use handle.status() as a live worker execution-state monitor.
Use it primarily to inspect the final asynchronous outcome.
Task result
result() reports the result stored in the Future state:
const auto result = handle.result();Possible values are:
none
success
failure
cancelled
timeout
rejectedBefore a terminal result has been stored:
result = noneAfter successful completion:
status = completed
result = success
error = okThe status and result APIs allow code to inspect completion without consuming the stored value.
Error state
Use:
const auto error = handle.error();For successful completion:
ThreadPoolErrc::okPossible ThreadPool-specific failures include values such as:
cancelled
timeout
rejectedA valid handle that has not completed yet also reports ok, because its asynchronous state has not stored an error.
Use ready() together with error() when the distinction matters:
if (handle.ready() && handle.error() != vix::threadpool::ThreadPoolErrc::ok)
{
// A threadpool error completed the result.
}Access the underlying Future
The handle exposes its Future directly:
auto& future = handle.future();A const overload is also available:
const auto& future = std::as_const(handle).future();This is useful when an API requires a Future<T>& or when the caller needs operations not duplicated by TaskHandle.
For example, TaskHandle does not expose wait_for() directly.
Use the Future:
const auto status = handle.future().wait_for(
std::chrono::milliseconds{100}
);The underlying Future remains owned by the handle.
Calling get() through either interface consumes the same asynchronous result.
Access the cancellation source
The cancellation source is also exposed:
auto& source = handle.cancellation_source();Requesting cancellation through it is equivalent to calling:
handle.cancel();For example:
handle.cancellation_source().request_cancel();The const overload can be used to inspect the source:
const bool requested = handle.cancellation_source().cancelled();For ordinary cancellation, prefer handle.cancel() because it expresses the intent directly.
Do not reset the cancellation source of a running handle
CancellationSource has a reset() operation that creates a new cancellation state.
The submitted task keeps observing the cancellation state created when handle() performed the submission.
Therefore, resetting the source exposed by a live handle separates future cancellation requests from the token already associated with that task.
Avoid:
handle.cancellation_source().reset();while the task is still pending or executing.
Use the cancellation source as the control channel created for that handle.
Cancellation options passed to handle()
handle() creates its own CancellationSource so that TaskHandle::cancel() can control the submitted task.
As part of this process, the handle-owned cancellation token replaces any cancellation token already present in the supplied TaskOptions.
For example:
vix::threadpool::TaskOptions options;
options.set_priority(vix::threadpool::TaskPriority::high);
auto handle = pool.handle([](){
return 42;
}, options);priority and the other task options remain meaningful, but cancellation is controlled by the source owned by the returned handle.
If work must use an externally supplied CancellationToken, use the normal task-option path described in Cancellation.
Rejected handles
Unlike post(), handle() does not return a boolean acceptance result.
If the pool cannot accept the task, the returned handle contains a ready Future with a rejection error.
For example:
vix::threadpool::ThreadPool pool(1);
pool.shutdown();
auto handle = pool.handle([](){
return 42;
});The handle still contains a task ID and Future state.
The result is represented asynchronously:
ready true
status rejected
result rejected
error rejectedCalling:
handle.get();throws std::system_error.
This is why handle.valid() should not be used as an acceptance check.
Pre-reserve a task ID
Some higher-level systems need the task identifier before they build the callable.
ThreadPool provides:
const auto id = pool.next_task_id();The reserved ID can then be used with:
auto handle = pool.handle_with_id(id, [id](){
return id;
});The returned handle contains the supplied identifier:
if (handle.id() != id)
{
return 1;
}The normal pattern is:
next_task_id()
↓
construct state that uses the ID
↓
handle_with_id()Most application code does not need this API. Use handle() when the identifier does not need to exist before submission.
handle_with_id() does not generate another ID
When using:
const auto id = pool.next_task_id();
auto handle = pool.handle_with_id(id, [](){
return 42;
});handle_with_id() uses the ID supplied by the caller.
It does not allocate an additional task ID.
Use IDs returned by next_task_id() rather than inventing arbitrary values when task identity is meant to follow the pool's normal ID sequence.
Move-only semantics
TaskHandle<T> is move-only because its Future<T> is move-only.
Copying is disabled:
copy construction disabled
copy assignment disabledMoving is supported:
auto first = pool.handle([](){
return 42;
});
auto second = std::move(first);After the move, second owns the Future and cancellation source.
The moved-from handle should not be used as a valid task handle.
A handle does not keep the pool alive
TaskHandle stores:
TaskId
Future
CancellationSourceIt does not store or own the ThreadPool.
Conceptually:
ThreadPool
↓
submits task
TaskHandle
↓
observes result and cancellation statePool lifetime and handle lifetime are separate.
A handle can retain its asynchronous state independently, but correct task completion still depends on the execution lifecycle of the pool that accepted the work.
Typical workflow
A normal task-handle workflow is:
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
auto handle = pool.handle([](){
return 42;
});
if (!handle.valid())
{
return 1;
}
const auto id = handle.id();
const int result = handle.get();
return id != vix::threadpool::invalid_task_id && result == 42 ? 0 : 1;
}When cancellation is required:
auto handle = pool.handle([](){
return perform_work();
});
if (work_is_no_longer_needed())
{
handle.cancel();
}
try
{
auto result = handle.get();
}
catch (const std::system_error&)
{
// Cancellation, rejection, or another threadpool error.
}The important model is:
ThreadPool::handle()
↓
TaskHandle
├── identity
├── Future
└── cancellation request
↓
worker execution
↓
terminal asynchronous resultUse Futures and Promises for the result model and Cancellation for the detailed cancellation semantics.