[](https://crates.io/crates/cancel-this)
[](https://docs.rs/cancel-this/)
[](https://github.com/daemontus/cancel-this/actions/workflows/build.yml)
[](https://bencher.dev/perf/cancel-this/)
[](https://codecov.io/gh/daemontus/cancel-this)
[](https://github.com/daemontus/cancel-this/issues)
[](https://github.com/daemontus/cancel-this/commits/main)
[](https://github.com/daemontus/cancel-this/blob/main/LICENSE)
# `cancel_this` (Rust co-op cancellation)
This crate provides a user-friendly way to implement cooperative
cancellation in Rust based on a wide range of criteria, including
*triggers*, *timers*, *OS signals* (Ctrl+C), or the *Python
interpreter linked using PyO3*. It also provides liveness monitoring
of "cancellation aware" code.
**Why not use `async` instead of cooperative cancellation?** In principle,
`async` was designed to solve a different problem, and that's executing IO-bound
tasks in a non-blocking fashion. It is not *really* designed for CPU-bound tasks.
Consequently, using `async` adds a lot of unnecessary overhead to your project
which `cancel_this` does not have (see also the *Performance* section below).
**Why not use [`stop-token`](https://crates.io/crates/stop-token),
[`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html)
or other cooperative cancellation crates?** So far, all crates I have seen require you
to pass the cancellation token around and generally do not make it easy to
combine the effects of multiple tokens. In `cancel_this`, the goal was to
make cancellation dead simple: You register however many cancellation triggers
you want, each trigger is valid within a specific scope (and thread), and can be checked
by a macro anywhere in your code.
### Current features
- Scoped cancellation using thread-local "cancellation triggers".
- Out-of-the box support for triggers based on atomics and timers.
- With feature `ctrlc` enabled, support for cancellation using `SIGINT` signals.
- With feature `pyo3` enabled, support for cancellation using `Python::check_signals`.
- With feature `liveness` enabled, you can register a per-thread handler invoked
once the thread becomes unresponsive (i.e. cancellation is not checked periodically
withing the desired interval).
- Practically no overhead in cancellable code when cancellation is not enabled.
- Very small overhead for "atomic-based" cancellation triggers, acceptable overhead for PyO3 cancellation.
- All triggers and guards generate [`log`](https://crates.io/crates/log) messages (`trace` for normal operation,
`warn` for issues where panic can be avoided).
### Simple example
A simple counter that is eventually cancelled by a one-second timeout. More complex examples (including
liveness monitoring and multi-threaded usage) are provided in the [documentation](https://docs.rs/cancel-this/).
```rust
use std::time::Duration;
use cancel_this::{Cancellable, is_cancelled};
fn cancellable_counter(count: usize) -> Cancellable<()> {
for _ in 0..count {
is_cancelled!()?;
std::thread::sleep(Duration::from_millis(10));
}
Ok(())
}
fn main() {
let one_s = Duration::from_secs(1);
let result: Cancellable<()> = cancel_this::on_timeout(one_s, || {
cancellable_counter(5)?;
cancellable_counter(10)?;
cancellable_counter(100)?;
Ok(())
});
assert!(result.is_err());
}
```
### Performance
The overall overhead of adding cancellation checks will **heavily** depend on how often they are performed.
Under ideal conditions, you don't want to run them too often. However, delaying cancellation too much can make
your code seem unresponsive. In `./benches`, we provide a benchmark to illustrate the impact of cancellation
on simple code. Here, we intentionally use cancellation checks too often to gain significant overhead. In your
own code, it is typically sufficient to run cancellation every few milliseconds.
#### Caching cancellation triggers
If you need to check cancellation repeatedly in a performance sensitive piece of code, you might want to
sacrifice some ergonomics of `cancel_this` for reduced overhead. In such cases, you can use
`cancel_this::active_triggers` to store a "local copy" of all active triggers. You can then pass such triggers
directly to `is_cancelled!` to avoid a (relatively) costly thread-local variable access.
#### Sample results
Benchmarks with `liveness=true` are running with liveness monitoring (this adds additional overhead).
The `synchronous` benchmark is a baseline without any cancellation support.
The `async::tokio` benchmark implements cancellation using `async` functions.
The `cancellable::none` benchmark implements cancellation using `cancel_this`, but with no trigger registered.
Benchmarks marked as `cached` use a local variable the cache the active triggers.
Remaining benchmarks test different "cancellation triggers" implemented in `cancel_this`.
These results were obtained
on a M2 Max Macbook Pro using `cargo bench` (the exact output is simplified for brevity). Latest results
from a more stable desktop environment are also available on [bencher.dev](https://bencher.dev/perf/cancel-this/)
or in the relevant [CI run](https://github.com/daemontus/cancel-this/actions/workflows/bench_base.yml).
```
hash::synchronous; 4.0006 µs
hash::async::tokio; 17.076 µs
hash::cancellable::none; (liveness=false) 4.0369 µs
hash::cancellable::none; (liveness=true) 7.6464 µs
hash::cancellable::none::cached; (liveness=false) 4.0020 µs
hash::cancellable::none::cached; (liveness=true) 4.0214 µs
hash::cancellable::atomic; (liveness=false) 4.9599 µs
hash::cancellable::atomic; (liveness=true) 7.6691 µs
hash::cancellable::atomic::cached; (liveness=false) 4.0318 µs
hash::cancellable::atomic::cached; (liveness=true) 4.0614 µs
hash::cancellable::timeout; (liveness=false) 4.9626 µs
hash::cancellable::timeout; (liveness=true) 7.7143 µs
hash::cancellable::sigint; (liveness=false) 4.9717 µs
hash::cancellable::sigint; (liveness=true) 7.7038 µs
hash::cancellable::python; (liveness=false) 79.738 µs
hash::cancellable::python; (liveness=true) 82.695 µs
```
To run the benchmarks locally, simply use `cargo bench --all-features` (with liveness turned on) or
`cargo bench --features=ctrlc --features=pyo3` (liveness turned off).