# windows-threadpool-sys
Memory-safe Rust access to the Windows thread pool APIs.
The Windows thread pool integrates work, timers, waits, and asynchronous I/O
with the operating system's own scheduling facilities. Its distinguishing
property is that an idle workload costs no threads at all: a process waiting on
timers, events, or I/O holds no dedicated thread stacks. This crate wraps those
facilities while making callback and resource lifetimes explicit in Rust.
## The object types
Each thread-pool object is an owned Rust type whose `Drop` performs the
documented teardown for that object, so a callback can never outlive the state
it captured.
| `work::ThreadpoolWork` | `TP_WORK` | you submit it |
| `timer::ThreadpoolTimer` | `TP_TIMER` | a due time arrives, once per arming |
| `timer::ThreadpoolPeriodicTimer` | `TP_TIMER` | every period, until stopped |
| `wait::ThreadpoolWait` | `TP_WAIT` | a handle signals or a wait times out |
| `io::ThreadpoolIo` | `TP_IO` | an overlapped operation completes |
One-shot and periodic timers are separate types on purpose. The platform models
both with one object and a `period` argument, which hides the property that
matters most when writing the callback: a `ThreadpoolPeriodicTimer` may queue its next
tick while the previous one is still running, so its callback must tolerate
overlapping with itself. Re-arming a `ThreadpoolTimer` from inside its own
callback never overlaps — the request is applied only after the callback
returns, so the gap is measured from the end of each firing. Arming a one-shot
from outside while its callback is running is the one case that can still
overlap it.
Three supporting types shape where those callbacks run and how they are torn
down: `pool::ThreadpoolPool` is an owned private pool,
`callback_env::CallbackEnviron` is the environment that selects a pool and a
callback priority when an object is created, and `cleanup_group::CleanupGroup`
releases many objects in one step instead of dropping each individually. The
callback environment provides Rust equivalents of the SDK's header-only inline
helpers, which `windows-sys` cannot emit.
A cleanup group creates its own members, so the borrow checker prevents using
one after the group has released it. Thread-pool I/O is deliberately excluded: a
`TP_IO` object must not be closed while an overlapped operation is outstanding,
and a bulk release cannot satisfy that precondition.
## Example
```rust
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use windows_threadpool_sys::work::ThreadpoolWork;
let count = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&count);
}, None).expect("create work");
for _ in 0..4 {
work.submit();
}
work.wait();
assert_eq!(count.load(Ordering::SeqCst), 4);
```
More examples, including timers, waits, private pools, and overlapped I/O, are
in the [API documentation](https://docs.rs/windows-threadpool-sys).
## Callback rules
Callbacks run on shared, process-managed threads, so every object type holds its
callback to the same contract: restore any thread state you change, do not
terminate the thread, and do not block waiting on your own object's rundown. A
callback must not panic — a panic unwinds to the `extern "system"` trampoline,
where an escaping unwind aborts the process. Nothing contains it. The panic hook
still runs first, so the message and location reach stderr by default; what is
given up is the process, not the diagnostic. A callback that can fail must
handle its own errors rather than panicking.
## Safety highlights
Each type carries the invariant its SDK object needs, rather than leaving it to
the caller to remember:
- **Waits own a handle of proven provenance.** The pool supports only some kinds
of handle — a mutex handle, for instance, makes the native behaviour undefined
with no error returned — so `ThreadpoolWait` takes a `WaitableHandle` rather
than any `OwnedHandle`. It converts safely from an `Event` of the
`win-sync-sys` crate -- an event is never a mutex -- and offers one narrow
`unsafe` seam for handles obtained elsewhere, which keeps unsupported handles out of the safe API instead of
documenting a rule the caller must remember. The wait then owns the handle and
hands it back only as a borrow, so it cannot be closed underneath a pending
wait.
- **Periodic timers reject a period they cannot honour.** The pool takes the
period in whole milliseconds, so anything shorter rounds to zero and means "do
not repeat". `ThreadpoolPeriodicTimer` rejects periods below `MIN_PERIOD`
rather than returning a "periodic" timer that fires once.
- **Callbacks get a token for the operations only they can perform.**
`TimerFiring::rearm_after` re-arms a one-shot from inside its own firing,
`PeriodicTick::stop` lets a periodic timer end itself, and
`WaitActivation::rearm` re-arms a wait — which the SDK requires per activation.
- **Cleanup-group members are protected at compile time.** Members borrow the
group and `close_members` takes `&mut self`, so using a member after the group
released it is a borrow-check error rather than a documented rule.
- **A panicking callback aborts.** Nothing contains an unwind at the trampoline;
the callback contract forbids panicking, and a violation ends the process
rather than being silently forgiven.
- **Teardown is ordered.** Every object disarms or cancels before draining
callbacks, then releases its callback context last, so a callback can never
outlive the state it captured.
- **Teardown never asks the kernel to discard.** `Drop` and `stop_and_drain`
pass `FALSE` to the `WaitFor*Callbacks` rundown, so they wait for what is
queued rather than requesting its removal. That is partly because discarding
work the caller asked for is not finalisation, and partly because the removal
primitive can sever a completion port's notification to its worker factory --
after which that pool dispatches nothing at all. What is structural is that
no teardown path reaches that primitive.
**That is narrower than "every queued callback runs", and the difference is
the timers.** A timer teardown disarms before it drains, and a disarm
discards a tick that is already queued -- measured for the one-shot by
`disarming_cancels_a_queued_tick_which_a_waits_disarm_does_not` and for the
periodic by `stopping_a_periodic_timer_discards_a_tick_that_is_already_queued`.
A wait's disarm does not, which is the asymmetry those test names carry. So
do not rely on a queued *tick* running during teardown; the guarantee here is
about which primitive the rundown invokes.
## Features
| `self-heal` | **on** | Repairs a thread pool that an explicitly-requested cancellation may have wedged, and provides the safe `try_cancel_pending`. See [README-FEATURE-self-heal.md](README-FEATURE-self-heal.md) before turning it off -- the hazard it covers is silent, intermittent, and lands on components that never called the API. |
| `trace` | off | In-process tracing of this crate's own operations: object creation, callback entry and exit, teardown, and the obligations each type carries. Diagnostic, and observe-only -- it records what this crate does and reaches no further. |
| `fail-fast` | off | Turns a teardown that found a drain still owed into a panic rather than a report. Arms the check directly -- see the warning below before enabling it anywhere but a leaf binary. |
### `fail-fast` cannot be declined by the crates it affects
Cargo unifies features across a build, so **any** crate enabling `fail-fast`
turns it on for every crate in that build. A library that depends on this one
cannot opt out of another dependency's choice, and `default-features = false`
does not help: the feature is off by default, so declining the defaults declines
nothing here.
What changes is teardown behaviour process-wide -- a `Drop` that previously
reported an outstanding drain now panics. That is a reasonable thing for an
application to ask for about its own code, and an unreasonable thing to impose
on an unrelated component that happens to share the build.
So enable it from a binary, a test, or a development profile, and not from a
published library's default feature set.
### No undocumented APIs
The `trace` feature records what this crate itself does: object creation,
callback entry and exit, teardown, and the obligations each type carries. It
reaches no further than that.
An earlier version of this crate also carried an inline hooking facility that
patched `ntdll` syscall stubs to observe the worker factory. **It has been
removed**, and with it every undocumented entry point and every assumption about
an unpublished structure layout. The reasoning, and what rebuilding the same
observations on documented ground would take, is in
[DESIGN-NOTES.md](../../DESIGN-NOTES.md#no-undocumented-apis).
## Relationship to `windows-overlapped-io-sys`
Thread-pool I/O is one of three completion backends for the overlapped model
defined by [`windows-overlapped-io-sys`](../windows-overlapped-io-sys). This
crate implements the `TP_IO` backend over that crate's endpoint ownership and
pinned operation storage, adding the balanced `StartThreadpoolIo` accounting
that only the thread pool requires. The pool's internal completion port is never
exposed.
`ThreadpoolIo::new` takes that crate's `UnassociatedEndpoint` and
`ThreadpoolIo::observer` returns its `EndpointObserver`, so those types are part
of this crate's public API. Each `windows-overlapped-io-sys` minor release in the
0.x series is therefore a minor release of this crate too: 0.3 of this crate
pairs with 0.3 of `windows-overlapped-io-sys`.
## Timer stress suite
[`tests/timer_stress.rs`](tests/timer_stress.rs) applies sustained load to the
timer types: self-re-arming chains, arming churn from many threads, teardown
racing live callbacks, deliberately overlapping periodic ticks, cleanup groups
holding armed members, and a mixed scenario running all of it at once.
It is **opt-in** and deliberately excluded from CI, where it would be slow and
where a contended shared runner makes timing-sensitive scenarios unreliable.
Nothing runs unless `WINDOWS_THREADPOOL_STRESS` is set:
```powershell
$env:WINDOWS_THREADPOOL_STRESS = "1"
cargo test -p windows-threadpool-sys --test timer_stress -- --nocapture
```
`WINDOWS_THREADPOOL_STRESS_SCALE` multiplies every load count, so the same
scenarios run harder without editing them:
```powershell
$env:WINDOWS_THREADPOOL_STRESS_SCALE = "10"
```
The suite still compiles and lints in CI, so it cannot rot; only the load is
skipped. A full run at scale 1 takes about a minute.
Two things worth knowing before reading the output. Pool timers fire on the
system timer tick (~15.6ms measured), so a zero-delay re-arm chain advances at
roughly 64 links a second however trivial the callback -- scenarios are sized
for wall-clock time rather than round iteration counts. And a loop that arms and
disarms without pausing outruns the pool entirely, never reaching a tick with the
timer armed; scenarios that need firings pause past a tick and assert a floor, so
they cannot silently degenerate into testing the arming calls alone.
Assertions are limited to what is invariant under load: non-overlap where the
type guarantees it, quiescence after a drain, and the absence of a hang or a
crash. Rates, latencies, and exact firing counts are reported rather than
asserted, because under load those describe the machine rather than the code.
## Status
Work, timers, waits, private pools, cleanup groups, and thread-pool I/O are
implemented and tested. `CallbackEnviron::set_cleanup_group` remains `unsafe` as
a raw seam for foreign cleanup groups; use `CleanupGroup` for a safe one.
This crate is Windows-only. Every item is behind `cfg(windows)`, so the crate
builds to an empty one on other targets rather than failing to compile, and
CI builds, tests, and lints exclusively on Windows.
## License
MIT. Copyright (c) Mike Grier.