Cancellation
The ThreadPool module provides cooperative cancellation through CancellationSource and CancellationToken.
The model is:
CancellationSource
│
│ creates
▼
CancellationToken
│
│ observes
▼
taskThe source requests cancellation. Tokens observe that request.
Cancellation does not forcibly terminate a C++ function that is already executing.
Basic cancellation
Create a CancellationSource, obtain its token, and attach the token to TaskOptions:
vix::threadpool::CancellationSource source;
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
source.token()
);
auto future = pool.submit([](){
return 42;
}, options);Request cancellation with:
source.request_cancel();Every token sharing that cancellation state can then observe the request.
CancellationSource
CancellationSource owns the shared cancellation state.
vix::threadpool::CancellationSource source;A new source starts in the non-cancelled state:
source.cancelled() == falseRequest cancellation with:
source.request_cancel();After the request:
source.cancelled() == trueThe alias:
source.is_cancelled();reports the same state.
Cancellation is idempotent
Calling request_cancel() several times is safe:
source.request_cancel();
source.request_cancel();
source.request_cancel();The state remains cancelled.
Conceptually:
not cancelled
↓
request_cancel()
↓
cancelled
↓
request_cancel()
↓
cancelledThere is no transition back to the original non-cancelled state.
CancellationToken
A CancellationToken observes the state owned by a source.
vix::threadpool::CancellationSource source;
vix::threadpool::CancellationToken token = source.token();Check whether the token is connected to a cancellation state:
const bool connected = token.can_cancel();For a token created by a source:
can_cancel() = trueA default-constructed token is disconnected:
vix::threadpool::CancellationToken token;and reports:
can_cancel() false
cancelled() false
stop_requested() false
can_continue() trueA disconnected token never becomes cancelled.
Observe cancellation
The primary check is:
if (token.cancelled())
{
// Cancellation was requested.
}The alias:
token.is_cancelled();provides the same result.
stop_requested() also reports whether cancellation was requested:
if (token.stop_requested())
{
// Stop has been requested.
}can_continue() provides the opposite view:
if (token.can_continue())
{
// Cancellation has not been requested.
}The relationships are:
cancelled() == true
stop_requested() == true
can_continue() == falseor:
cancelled() == false
stop_requested() == false
can_continue() == trueSource and token share state
The source and its tokens refer to the same CancellationState.
vix::threadpool::CancellationSource source;
auto token = source.token();
source.request_cancel();
if (!token.cancelled())
{
return 1;
}Conceptually:
shared state
cancelled=true
▲ ▲
│ │
CancellationSource CancellationTokenCancellation state uses an atomic flag and can be observed safely across threads.
Multiple tokens
One source can create multiple tokens:
vix::threadpool::CancellationSource source;
auto first = source.token();
auto second = source.token();
auto third = source.token();
source.request_cancel();All of them observe the same request:
first.cancelled() true
second.cancelled() true
third.cancelled() trueThis allows several related operations to share one cancellation signal.
CancellationSource copies share state
CancellationSource can be copied.
Copies continue to refer to the same cancellation state:
vix::threadpool::CancellationSource first;
vix::threadpool::CancellationSource second = first;
auto token = first.token();
second.request_cancel();
if (!token.cancelled())
{
return 1;
}Conceptually:
Source A ──┐
├──► CancellationState
Source B ──┘ ▲
│
TokenA cancellation request through either source copy becomes visible through all tokens connected to that state.
Attach cancellation to a task
Use TaskOptions::with_cancellation():
vix::threadpool::CancellationSource source;
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
source.token()
);
auto future = pool.submit([](){
return 42;
}, options);The setter form is:
vix::threadpool::TaskOptions options;
options.set_cancellation(
source.token()
);Check whether options contain a connected token with:
if (options.has_cancellation())
{
// This task has a cancellation channel.
}Cancellation before submit()
If cancellation has already been requested before submit() is called:
vix::threadpool::CancellationSource source;
source.request_cancel();
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
source.token()
);
auto future = pool.submit([](){
return 42;
}, options);the callable is not submitted for normal execution.
The Future is completed as cancelled:
ready() true
status() cancelled
result() cancelled
error() cancelledCalling:
future.get();throws std::system_error.
The cancellation request is therefore observable without running the user callable.
Cancellation while queued
Cancellation can also be requested after submission but before the worker reaches the callable.
Conceptually:
submit task
↓
task waits in queue
↓
request cancellation
↓
worker reaches task
↓
cancellation observed
↓
callable skippedThis is one of the main uses of task cancellation.
For a submit() operation, the Future completes with:
status = cancelled
result = cancelled
error = cancelledMake queued cancellation observable
A single-worker pool can be used to illustrate the timing:
#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}
);
});
vix::threadpool::CancellationSource source;
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
source.token()
);
auto future = pool.submit([](){
return 42;
}, options);
source.request_cancel();
blocker.get();
return future.status() ==
vix::threadpool::TaskStatus::cancelled
? 0
: 1;
}The first task keeps the only worker occupied while cancellation is requested for the second task.
When the worker reaches the second submission, the callable is skipped.
TaskHandle cancellation
ThreadPool::handle() creates a cancellation source automatically.
auto handle = pool.handle([](){
return 42;
});Request cancellation with:
handle.cancel();Check whether the request has been made with:
handle.cancelled();Conceptually:
TaskHandle
│
├── Future
│
└── CancellationSource
│
▼
task wrapperThis avoids creating a separate CancellationSource when task-level control is required.
See Task Handles.
Handle cancellation while queued
A task handle is especially useful when work may still be waiting in a queue:
#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;
}handle.cancelled() means that cancellation was requested.
To determine the asynchronous outcome, inspect:
handle.status();
handle.result();
handle.error();or consume it with:
handle.get();Cancellation request and cancellation result are different
This distinction is important.
handle.cancelled();answers:
Was cancellation requested?while:
handle.status() ==
vix::threadpool::TaskStatus::cancelledanswers:
Did the asynchronous operation finish
through the cancellation path?These conditions can differ.
A cancellation request can happen after the callable has already started, in which case the current handle() result path may still complete successfully.
Running C++ code is not forcibly interrupted
Cancellation does not kill a worker thread.
Suppose a task has already started:
worker
↓
user callable starts
↓
request_cancel()
↓
user callable continuesThe ThreadPool does not inject an exception, terminate the thread, or stop arbitrary machine instructions.
The callable continues until its own code returns or throws.
This avoids unsafe asynchronous termination of C++ code.
Cooperative cancellation inside a callable
For long-running work, the callable can explicitly observe a token.
#include <chrono>
#include <thread>
#include <vix/threadpool/all.hpp>
int main()
{
vix::threadpool::ThreadPool pool(4);
vix::threadpool::CancellationSource source;
auto token = source.token();
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
token
);
auto future = pool.submit([token](){
for (int i = 0; i < 100; ++i)
{
if (token.stop_requested())
{
return false;
}
std::this_thread::sleep_for(
std::chrono::milliseconds{1}
);
}
return true;
}, options);
source.request_cancel();
return 0;
}The important part is:
if (token.stop_requested())
{
return false;
}The task body decides where it is safe to stop.
This is cooperative cancellation.
TaskOptions do not inject the token into the callable
Attaching:
options.set_cancellation(source.token());does not automatically add a CancellationToken argument to the callable.
This is not valid:
pool.submit([](vix::threadpool::CancellationToken token){
// The pool does not inject this argument.
}, options);When the callable itself needs to observe cancellation, capture the token explicitly:
auto token = source.token();
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
token
);
auto future = pool.submit([token](){
if (token.stop_requested())
{
return 0;
}
return perform_work();
}, options);The token used by TaskOptions and the captured token observe the same shared state.
submit() cancellation checks
The current submit() path observes cancellation at two points before the user callable runs.
First, before the task is sent to the scheduler:
submit()
↓
cancellation already requested?
│
├── yes → Future cancelled
│
└── no → continueThen, when the worker invokes the result-producing wrapper:
worker reaches task
↓
cancellation requested?
┌───────┴───────┐
yes no
│ │
Future cancelled invoke callableThis covers cancellation that occurs while the task is waiting in a queue.
submit() after the callable starts
Once the user callable begins, the current submit() wrapper does not perform another cancellation check after the callable returns.
Therefore:
callable starts
↓
cancellation requested
↓
callable keeps running
↓
callable returns value
↓
Future can complete successfullyunless the callable itself observes the cancellation token and changes its own behavior.
This is an important part of the current contract.
Cancellation of running submit() work is cooperative at the callable level.
TaskHandle after the callable starts
TaskHandle::cancel() follows the same principle.
The handle-owned cancellation token is checked immediately before the user callable is invoked.
If cancellation is requested after the callable has started:
handle callable starts
↓
handle.cancel()
↓
request recorded
↓
callable continues
↓
callable returns
↓
Future can report successFor example:
handle.cancelled();can be true while:
handle.result();eventually becomes:
successDo not interpret handle.cancelled() as proof that a running callable was stopped.
Low-level Task cancellation
The low-level Task::run() behavior is slightly different because its TaskOptions remain attached directly to the Task.
Before invoking its callable, Task::run() checks:
cancellation requested?
↓
yes → cancelled without invocationAfter the callable returns, it checks cancellation again.
Conceptually:
check cancellation
↓
run callable
↓
check cancellation againIf cancellation was requested during execution, the low-level task can finish with:
status = cancelled
result = cancelledeven though the callable itself ran to completion.
post() uses low-level Task cancellation
ThreadPool::post() stores the cancellation token directly in the low-level task:
vix::threadpool::CancellationSource source;
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
source.token()
);
const bool accepted = pool.post([](){
perform_work();
}, options);The low-level task checks cancellation before and after the callable.
This differs from the Future-producing submit() path, which moves cancellation observation into its result wrapper and checks it before invoking the callable.
This difference matters when cancellation is requested while a callable is already running.
post() with an already cancelled token
Unlike submit(), post() does not complete a Future before queueing because it has no Future.
An already cancelled posted task can still be accepted by the scheduler:
vix::threadpool::CancellationSource source;
source.request_cancel();
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
source.token()
);
const bool accepted = pool.post([](){
perform_work();
}, options);If accepted into the runtime, post() can return:
truewhile the low-level task later observes cancellation and skips the callable.
The boolean from post() means:
the submission was acceptednot:
the callable executed successfullyCancellation result for post()
Posted work has no Future.
Its cancellation outcome is therefore not retrieved with:
future.get();The low-level worker records the task as cancelled, which contributes to runtime metrics and statistics.
For application-level completion or cancellation reporting, use submit() or handle() when a result object is required.
Reset a CancellationToken
A token can disconnect itself from its current state:
vix::threadpool::CancellationSource source;
vix::threadpool::CancellationToken token = source.token();
token.reset();After reset:
token.can_cancel() false
token.cancelled() falseLater cancellation through the original source is no longer visible through that token.
Conceptually:
before reset:
Token ─────► CancellationState
after reset:
Token CancellationState
X──────────────►Resetting one token does not affect the source or other tokens.
Reset a CancellationSource
A source can be reset:
source.reset();This creates a new non-cancelled CancellationState.
Existing tokens remain connected to the previous state.
For example:
vix::threadpool::CancellationSource source;
auto oldToken = source.token();
source.reset();
auto newToken = source.token();
source.request_cancel();The result is:
oldToken.cancelled() false
newToken.cancelled() truebecause the cancellation request affects the source's new state.
Conceptually:
oldToken ─────► old state
source ───────► new state ◄──── newToken
│
▼
cancelledReset does not migrate existing tokens to the new state.
Do not reset a live task's cancellation source
If a token has already been attached to submitted work, resetting the source creates a different cancellation channel.
For example:
auto token = source.token();
vix::threadpool::TaskOptions options =
vix::threadpool::TaskOptions::with_cancellation(
token
);
auto future = pool.submit([](){
return 42;
}, options);
source.reset();
source.request_cancel();the submitted task continues observing the old state.
The cancellation request sent through the reset source affects only the new state.
Keep the original source state alive for the lifetime of the cancellation relationship.
Scope cancellation
Scope owns a shared cancellation source for its spawned work.
vix::threadpool::Scope scope(pool);Request cancellation with:
scope.cancel();Inspect it with:
scope.cancelled();or obtain the shared token:
auto token = scope.cancellation_token();Every task spawned through the scope receives the scope cancellation token in its TaskOptions.
Conceptually:
Scope
│
└── CancellationSource
│
┌─────┼─────┐
▼ ▼ ▼
task task taskSee Scopes.
Scope cancellation before spawn
A cancelled scope can still accept a spawn() operation for tracking while the underlying submitted callable is skipped by the cancellation path.
Conceptually:
scope.cancel()
↓
scope.spawn(task)
↓
scope token already cancelled
↓
Future completed as cancelled
↓
callable not executedScope::spawn() returning true means the operation was accepted for scope tracking.
It does not mean that a cancelled callable actually executed.
TaskGroup cancellation
TaskGroup also owns a shared cancellation source:
vix::threadpool::TaskGroup group;
auto token = group.cancellation_token();
group.cancel();After cancellation:
group.cancelled() true
token.cancelled() trueUnlike Scope, TaskGroup is primarily a coordination and accounting object. It does not itself submit a callable to the pool.
Code connecting actual tasks to a TaskGroup must use the group's token as part of the task execution design.
See Task Groups.
Cancellation and deadlines
Cancellation and deadlines can both prevent work from beginning.
TaskOptions::should_skip_before_run() returns true when:
cancellation requested
or
deadline expiredFor submit():
cancelled
↓
ThreadPoolErrc::cancelled
deadline expired
↓
ThreadPoolErrc::timeoutThey remain separate concepts.
Cancellation is an explicit request.
A deadline is a time condition.
See Deadlines.
Cancellation and timeout
Cancellation is also different from timeout.
Cancellation
↓
another part of the program requests stop
Timeout
↓
configured execution duration is exceededNeither mechanism forcibly terminates arbitrary C++ code.
A long-running operation that must react promptly should provide its own cooperative checkpoints.
See Timeouts.
Design cancellable work around safe checkpoints
A useful cancellable operation usually has natural places where it can stop safely.
For example:
auto token = source.token();
auto future = pool.submit([token](){
for (std::size_t i = 0; i < work_count(); ++i)
{
if (token.stop_requested())
{
return false;
}
process_item(i);
}
return true;
}, options);The loop checks cancellation between units of work.
Conceptually:
process one unit
↓
check cancellation
↓
process next unit
↓
check cancellation
↓
...This gives the operation control over cleanup, invariants, locks, and resource lifetime.
Cancellation is not thread termination
Do not design around this assumption:
request_cancel()
↓
worker thread immediately stopsThe actual model is:
request_cancel()
↓
shared atomic state becomes cancelled
↓
ThreadPool or task code observes the state
↓
work stops at an observation pointThe cancellation request itself does not:
terminate a worker thread
interrupt a system call
unlock application mutexes
roll back side effects
destroy the callable while it is runningThose concerns remain part of the callable's own design.
Choosing a cancellation API
Use an explicit CancellationSource and TaskOptions when several pieces of work should observe a shared external cancellation signal:
vix::threadpool::CancellationSource source;Use TaskHandle when one result-producing task needs direct cancellation control:
auto handle = pool.handle([](){
return 42;
});
handle.cancel();Use Scope::cancel() when several spawned operations belong to one structured lifetime:
vix::threadpool::Scope scope(pool);
scope.cancel();Use TaskGroup::cancel() when coordinating a manually managed group around a shared cancellation state.
The underlying mechanism remains the same:
CancellationSource
↓
shared CancellationState
↓
CancellationToken
↓
cooperative observationCancellation model summary
The core cancellation path is:
CancellationSource
│
▼
request_cancel()
│
▼
shared atomic state
│
▼
CancellationToken
│
▼
task observes requestThe important properties are:
- Cancellation is cooperative.
CancellationSourcerequests cancellation.CancellationTokenobserves cancellation.- A default token is disconnected and never reports cancellation.
- Source copies share the same cancellation state.
- Multiple tokens can observe the same source.
- Cancellation requests are idempotent.
submit()checks cancellation before scheduling and again before invoking the callable.- Queued
submit()work can be skipped and its Future completed as cancelled. handle()provides its own cancellation source.handle.cancel()does not forcibly stop a callable that has already started.- Running
submit()andhandle()callables must cooperate explicitly if they need to stop early. - The low-level
Taskchecks cancellation before and after its callable. post()uses that low-level cancellation behavior.post()acceptance does not mean that an already cancelled callable will execute.- Resetting a source creates a new state and does not reconnect existing tokens.
- Resetting a token disconnects only that token.
Scopeautomatically attaches its cancellation token to spawned work.TaskGroupprovides shared cancellation state but does not itself submit tasks.
Continue with Deadlines for absolute time limits or Timeouts for execution-duration observation.