# 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 offers safe constructors for the handle kinds this
crate creates itself and 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.
## 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.
## 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.