windows_threadpool_sys/lib.rs
1// Copyright (c) 2026 Mike Grier
2//! Memory-safe access to the Windows thread pool APIs.
3//!
4//! The Windows thread pool integrates work, timers, waits, and asynchronous I/O
5//! with the operating system's own scheduling facilities. Its distinguishing
6//! property is that an idle workload costs no threads at all: the pool and the
7//! kernel cooperate so a process waiting on timers, events, or I/O holds no
8//! dedicated thread stacks. This crate wraps those facilities while making
9//! callback and resource lifetimes explicit in Rust.
10//!
11//! # The object types
12//!
13//! Each thread-pool object is an owned Rust type whose `Drop` performs the
14//! documented teardown for that object, so callbacks can never outlive the state
15//! they capture:
16//!
17//! | Type | Wraps | Runs the callback when |
18//! |---|---|---|
19//! | [`work::ThreadpoolWork`] | `TP_WORK` | you submit it |
20//! | [`timer::ThreadpoolTimer`] | `TP_TIMER` | a due time arrives, once per arming |
21//! | [`timer::ThreadpoolPeriodicTimer`] | `TP_TIMER` | every period, until stopped |
22//! | [`wait::ThreadpoolWait`] | `TP_WAIT` | a handle signals or a wait times out |
23//! | [`io::ThreadpoolIo`] | `TP_IO` | an overlapped operation completes |
24//!
25//! One-shot and periodic timers are separate types on purpose. The platform
26//! models both with one object and a `period` argument, which hides the property
27//! that matters most when writing the callback: a [`timer::ThreadpoolPeriodicTimer`] may
28//! queue its next tick while the previous one is still running, so its callback
29//! must tolerate overlapping with itself, whereas a [`timer::ThreadpoolTimer`]
30//! re-armed from *inside* its callback never does -- that request is applied
31//! only once the callback returns. Arming a one-shot from outside while its
32//! callback runs can still overlap it; see the [`timer`] module for both the
33//! choice and that distinction.
34//!
35//! Three supporting types shape where those callbacks run and how they are torn
36//! down: [`pool::ThreadpoolPool`] is an owned private pool,
37//! [`callback_env::CallbackEnviron`] is the environment that selects a pool and
38//! a callback priority when an object is created, and
39//! [`cleanup_group::CleanupGroup`] releases many objects in one step instead of
40//! dropping each individually.
41//!
42//! # Submitting work
43//!
44//! ```
45//! use std::sync::Arc;
46//! use std::sync::atomic::{AtomicUsize, Ordering};
47//! use windows_threadpool_sys::work::ThreadpoolWork;
48//!
49//! let count = Arc::new(AtomicUsize::new(0));
50//! let counter = Arc::clone(&count);
51//!
52//! let work = ThreadpoolWork::new(move || {
53//! counter.fetch_add(1, Ordering::SeqCst);
54//! }, None)?;
55//!
56//! for _ in 0..4 {
57//! work.submit();
58//! }
59//! work.wait();
60//!
61//! assert_eq!(count.load(Ordering::SeqCst), 4);
62//! # Ok::<(), std::io::Error>(())
63//! ```
64//!
65//! # Running callbacks on a private pool
66//!
67//! A [`pool::ThreadpoolPool`] bounds the threads a subsystem may consume.
68//! Declare the pool before the objects that use it, so it is dropped last.
69//!
70//! ```
71//! use std::sync::Arc;
72//! use std::sync::atomic::{AtomicUsize, Ordering};
73//! use windows_threadpool_sys::callback_env::CallbackEnviron;
74//! use windows_threadpool_sys::pool::ThreadpoolPool;
75//! use windows_threadpool_sys::work::ThreadpoolWork;
76//!
77//! let pool = ThreadpoolPool::new()?;
78//! pool.set_max_threads(2)?;
79//!
80//! let mut env = CallbackEnviron::new();
81//! env.set_pool(&pool);
82//!
83//! let count = Arc::new(AtomicUsize::new(0));
84//! let counter = Arc::clone(&count);
85//! let work = ThreadpoolWork::new(move || {
86//! counter.fetch_add(1, Ordering::SeqCst);
87//! }, Some(&mut env))?;
88//!
89//! work.submit();
90//! work.wait();
91//! assert_eq!(count.load(Ordering::SeqCst), 1);
92//! # Ok::<(), std::io::Error>(())
93//! ```
94//!
95//! # Callback rules
96//!
97//! Callbacks run on shared, process-managed threads, so every object type here
98//! holds its callback to the same contract:
99//!
100//! - It must restore any thread-local or thread state it changes before
101//! returning, and must not terminate its thread.
102//! - It must not block waiting on its own object's rundown, which would wait on
103//! itself.
104//! - It must not panic. A panic unwinds to the `extern "system"` trampoline,
105//! where an escaping unwind aborts the process; nothing contains it. The panic
106//! hook still runs first, so the message and location reach stderr by default
107//! -- what is given up is the process, not the diagnostic. A callback that can
108//! fail must handle its own errors rather than panicking.
109//!
110//! # Teardown drains, and why that is not a style preference
111//!
112//! Every teardown in this crate **drains**: it lets a callback that is already
113//! queued run, rather than asking the kernel to discard it. `Drop` blocks until
114//! that is finished, and [`wait::ThreadpoolWait::stop_and_drain`] is the same
115//! work at a point the caller chooses.
116//!
117//! That is partly a correctness argument -- a queued callback is work the caller
118//! asked for, and discarding it is not "finalised". It is also the only
119//! available defence against a measured fault in the Windows thread pool, which
120//! is worth stating plainly because the fault is silent, process-wide, and easy
121//! to attribute to anything else.
122//!
123//! ## The fault
124//!
125//! Tearing down a wait asks the kernel to cancel its completion packet
126//! (`IopCancelWaitCompletionPacket`). When the packet has **already been
127//! delivered** to the pool's completion port, that cancel removes it. If the
128//! removal lands a few microseconds after the packet was queued, on a pool that
129//! has **no threads yet**, the port's notification to its worker factory can be
130//! lost.
131//!
132//! Afterwards that pool dispatches **nothing**: not the waits already armed, not
133//! a freshly armed wait, not a timer, not a completed overlapped read. The
134//! factory meanwhile reads as perfectly healthy -- not paused, not shut down,
135//! permitted to create a worker, no failed creation, zero workers. Work queues
136//! up on the port and is never looked at.
137//!
138//! ## What it costs, and what recovers it
139//!
140//! The blast radius is one pool, and all of it. A second pool in the same
141//! process is unaffected. But the pool normally affected is the **default** one,
142//! so in practice that is every component in the process that did not create its
143//! own -- including code with no connection to whoever tore the wait down.
144//!
145//! Submitting a work item recovers it immediately and completely, because that
146//! reaches the factory by a route the lost notification is not on. Nothing else
147//! observed does. So a program that submits work items near its waits sees a
148//! **latency spike** easy to mistake for scheduler jitter; a program using only
149//! waits, timers and I/O has no stimulus that will ever help it and **hangs**.
150//!
151//! ## Why draining is the fix rather than a delay
152//!
153//! A gap between the disarm and the close also prevents it, and is **not** what
154//! this crate does. A gap makes the race improbable; draining makes it
155//! impossible, because `WaitForThreadpoolWaitCallbacks` with
156//! `fCancelPendingCallbacks` false cannot reach the removal primitive on any
157//! path. The queued callback runs, dispatch clears the association, and the
158//! close that follows finds nothing to take.
159//!
160//! Note the shape of that: **the close performs the same removal**, so no choice
161//! of entry point avoids the dangerous call. Draining does not dodge it -- it
162//! empties it. Avoiding
163//! [`wait::ThreadpoolWait::try_cancel_pending_no_heal_tracking`] buys nothing on
164//! its own, which is why the default paths drain rather than the hazardous call
165//! being hidden.
166//!
167//! ## If you are worried about code you do not control
168//!
169//! The fault needs a pool with no threads, which bounds the exposure to process
170//! start and to the moments after a pool's last worker retires.
171//! [`pool::prewarm_default_pool`] removes that precondition for as long as the
172//! pool stays warm. It is not a fix, and this crate's own teardowns do not need
173//! it.
174//!
175//! The worker-factory counters the diagnosis turned on are **not** readable from
176//! this crate: reading them needs an undocumented entry point and an unpublished
177//! structure layout, which this crate does not ship. The captures that were taken
178//! with them are under `measurements/`.
179//!
180//! Holding the pool warm permanently is not available: `SetThreadpoolThreadMinimum`
181//! does not accept the default pool, and calling it that way terminates the
182//! process rather than failing.
183//!
184//! ## Being told when a teardown had to drain: the `fail-fast` feature
185//!
186//! By default a teardown that finds a drain still owed records it and carries
187//! on. The `fail-fast` feature, off by default, turns that into a panic, so a
188//! caller who left the blocking drain to `Drop` finds out at the point it
189//! happened rather than from a trace afterwards.
190//!
191//! **Cargo unifies features across a build, so any crate that enables this
192//! turns it on for every crate in that build.** A library cannot decline
193//! another dependency's choice, and `default-features = false` does not help:
194//! the feature is off by default, so declining the defaults declines nothing.
195//! What changes is teardown behaviour process-wide, which is a reasonable thing
196//! for an application to ask about its own code and an unreasonable one to
197//! impose on an unrelated component sharing the build. Enable it from a binary,
198//! a test, or a development profile rather than from a published library's
199//! default feature set.
200//!
201//! # Relationship to `windows-overlapped-io-sys`
202//!
203//! Thread-pool I/O is one of three completion backends for the overlapped model
204//! defined by [`windows-overlapped-io-sys`]. This crate implements the `TP_IO`
205//! backend over that crate's endpoint ownership and pinned operation storage,
206//! adding the balanced `StartThreadpoolIo` accounting that only the thread pool
207//! requires. The pool's internal completion port is never exposed.
208//!
209//! [`windows-overlapped-io-sys`]: https://docs.rs/windows-overlapped-io-sys
210//!
211//! # Status
212//!
213//! The crate is in active development. Work, timers, waits, private pools,
214//! cleanup groups, and thread-pool I/O are implemented and tested.
215//!
216//! Thread-pool I/O is deliberately not a cleanup-group member: a `TP_IO` object
217//! must not be closed while an overlapped operation is outstanding, and a bulk
218//! release cannot satisfy that. See [`cleanup_group`] for the reasoning.
219
220#![warn(missing_docs)]
221
222// Every module wraps a Win32 thread-pool object, so the whole public surface is
223// gated on Windows and the crate resolves to an empty one elsewhere. This
224// matches the sibling `windows-overlapped-io-sys`, and it is what lets a
225// cross-platform dependency tree name this crate unconditionally instead of
226// failing to compile on other targets.
227#[cfg(windows)]
228pub mod callback_env;
229#[cfg(windows)]
230pub mod cleanup_group;
231#[cfg(windows)]
232pub(crate) mod heal;
233#[cfg(windows)]
234pub mod io;
235#[cfg(windows)]
236pub(crate) mod obligation;
237#[cfg(windows)]
238pub mod pool;
239#[cfg(windows)]
240pub(crate) mod rearm;
241#[cfg(windows)]
242pub mod timer;
243
244/// A trace for defects that only appear under concurrency, compiled out
245/// unless the `trace` feature is on and narrowed by environment variable when
246/// it is. See the module documentation for why an `eprintln!` is the wrong
247/// instrument for that class of problem.
248///
249/// Gated on Windows like every other Win32-backed module here: the traced
250/// build calls `GetCurrentThreadId`, so leaving it ungated would let
251/// `--features trace` break this crate's empty-on-other-targets behaviour.
252#[cfg(windows)]
253pub mod trace;
254#[cfg(windows)]
255pub mod wait;
256#[cfg(windows)]
257pub mod work;
258
259// The crate's markdown documentation is compiled as doctests, so an example that
260// a contract change invalidates breaks the build instead of quietly teaching the
261// old answer. `cfg(doctest)` means these items exist only while rustdoc collects
262// tests, so they cost an ordinary build nothing.
263#[cfg(all(doctest, windows))]
264#[doc = include_str!("../README.md")]
265struct ReadmeDoctests;