windows_threadpool_sys/pool.rs
1// Copyright (c) 2026 Mike Grier
2//! Owned private thread pools: `CreateThreadpool` / `CloseThreadpool`.
3//!
4//! Callbacks run on the process-default pool unless a [`CallbackEnviron`] names
5//! a private one. A private pool lets an application bound the threads a
6//! subsystem may consume, or isolate long-running callbacks from unrelated work,
7//! without affecting the rest of the process.
8//!
9//! [`CallbackEnviron`]: crate::callback_env::CallbackEnviron
10
11use std::io;
12use std::ptr;
13use std::sync::Mutex;
14
15use windows_sys::Win32::System::Threading::{
16 CloseThreadpool, CreateThreadpool, PTP_POOL, SetThreadpoolThreadMaximum,
17 SetThreadpoolThreadMinimum,
18};
19
20/// The thread limits this wrapper has been told about.
21///
22/// Each field is `None` until the corresponding setter succeeds, because Win32
23/// offers no way to read a pool's current limits back. A limit we were never
24/// given cannot be used to reject its counterpart.
25#[derive(Debug, Default)]
26struct Limits {
27 minimum: Option<u32>,
28 maximum: Option<u32>,
29}
30
31/// An owned private thread pool.
32///
33/// Pass it to [`CallbackEnviron::set_pool`] to run an object's callbacks on this
34/// pool instead of the process-default one. The environment borrows the pool, so
35/// the pool cannot be closed while an environment still names it.
36///
37/// # Ordering and teardown
38///
39/// Creating a callback object from an environment that names this pool **binds**
40/// the object to the pool. `CloseThreadpool` -- which this type's `Drop` calls --
41/// frees the pool immediately only when no object is bound; otherwise it defers
42/// the release until every bound object has been freed. A live object can
43/// therefore never observe a freed pool, whatever order the pool and its objects
44/// are dropped in, so this is not a memory-safety obligation on the caller. (The
45/// `CallbackEnviron` borrow of the pool covers the one case binding does not: a
46/// freshly created pool with no bound object yet is freed at once, so an
47/// environment must not outlive it.)
48///
49/// What the order *does* control is when teardown blocks. Each object's `Drop`
50/// waits for its in-flight callbacks; the pool's deferred release then completes
51/// once the last object is gone. Declare the pool before the objects that use
52/// it, so it is dropped last and that blocking happens where you expect.
53///
54/// # Examples
55///
56/// ```
57/// use windows_threadpool_sys::callback_env::CallbackEnviron;
58/// use windows_threadpool_sys::pool::ThreadpoolPool;
59/// use windows_threadpool_sys::timer::ThreadpoolTimer;
60/// use std::time::Duration;
61///
62/// // Declared first, so it outlives the objects that use it.
63/// let pool = ThreadpoolPool::new()?;
64/// pool.set_min_threads(1)?;
65/// pool.set_max_threads(4)?;
66///
67/// let mut env = CallbackEnviron::new();
68/// env.set_pool(&pool);
69///
70/// let timer = ThreadpoolTimer::new(|_firing| {}, Some(&mut env))?;
71/// timer.set_after(Duration::from_millis(1));
72/// // Discharges the drain this timer owes, at a point you choose rather than
73/// // leaving the blocking teardown to `Drop`.
74/// timer.stop_and_drain();
75/// # Ok::<(), std::io::Error>(())
76/// ```
77///
78/// [`CallbackEnviron::set_pool`]: crate::callback_env::CallbackEnviron::set_pool
79#[derive(Debug)]
80pub struct ThreadpoolPool {
81 pool: PTP_POOL,
82 limits: Mutex<Limits>,
83}
84
85// SAFETY: PTP_POOL is a kernel-managed object usable from any thread; this type
86// only owns the handle and hands it to the pool APIs, which are thread-safe.
87unsafe impl Send for ThreadpoolPool {}
88unsafe impl Sync for ThreadpoolPool {}
89
90impl ThreadpoolPool {
91 /// Create a new private thread pool.
92 ///
93 /// # Errors
94 ///
95 /// Returns the error from `CreateThreadpool`, which fails when the process
96 /// cannot allocate the pool.
97 pub fn new() -> io::Result<Self> {
98 // SAFETY: the reserved parameter must be null; no other input is read.
99 let pool = crate::trace_call!("CreateThreadpool", 0, 0, {
100 // SAFETY: a null reserved argument is the documented call.
101 unsafe { CreateThreadpool(ptr::null()) }
102 });
103 if pool == 0 {
104 return Err(io::Error::last_os_error());
105 }
106 Ok(Self {
107 pool,
108 limits: Mutex::new(Limits::default()),
109 })
110 }
111
112 /// Set the maximum number of threads this pool may allocate.
113 ///
114 /// # Conflicting limits
115 ///
116 /// Win32 lets the two limits contradict each other and resolves the conflict
117 /// by *last call wins*, silently and unreportably. A pool given a maximum of
118 /// 2 and then a minimum of 4 was measured running **4** callbacks
119 /// concurrently, and it did not settle back to 2. This wrapper therefore
120 /// tracks the limits it has set and rejects a pair that cannot both hold,
121 /// rather than letting one quietly annul the other.
122 ///
123 /// # The maximum is a steady-state target, not an instantaneous ceiling
124 ///
125 /// Even where the maximum is the effective limit, it bounds the pool once it
126 /// has settled, not every instant. Raising the minimum creates threads
127 /// eagerly, and those surplus threads are not retired the moment a lower
128 /// maximum is applied: with a minimum of 4 then a maximum of 2, a third
129 /// callback was observed running concurrently in roughly 1 trial in 240 when
130 /// many pools were being created at once. Do not rely on the maximum as a
131 /// mutual-exclusion mechanism; use it to bound resource consumption.
132 ///
133 /// # Errors
134 ///
135 /// Returns [`io::ErrorKind::InvalidInput`] if `maximum` is zero. Such a pool
136 /// runs no callbacks at all -- work submitted to it is queued and never
137 /// executed -- and `SetThreadpoolThreadMaximum` returns void, so nothing
138 /// else could report the mistake. Use
139 /// [`CleanupGroup`](crate::cleanup_group::CleanupGroup) or the objects' own
140 /// teardown to stop callbacks, rather than starving the pool that runs them.
141 ///
142 /// Also returns [`io::ErrorKind::InvalidInput`] if `maximum` is below a
143 /// minimum previously set through [`set_min_threads`](Self::set_min_threads).
144 ///
145 /// # Examples
146 ///
147 /// A maximum below an established minimum is refused instead of silently
148 /// overriding it:
149 ///
150 /// ```
151 /// use windows_threadpool_sys::pool::ThreadpoolPool;
152 ///
153 /// let pool = ThreadpoolPool::new()?;
154 /// pool.set_min_threads(4)?;
155 /// assert!(pool.set_max_threads(2).is_err());
156 /// # Ok::<(), std::io::Error>(())
157 /// ```
158 pub fn set_max_threads(&self, maximum: u32) -> io::Result<()> {
159 if maximum == 0 {
160 return Err(io::Error::new(
161 io::ErrorKind::InvalidInput,
162 "a thread pool needs a maximum of at least one thread; a maximum of zero runs no \
163 callbacks at all and the native call cannot report it",
164 ));
165 }
166 let mut limits = self.limits.lock().unwrap_or_else(|e| e.into_inner());
167 if let Some(minimum) = limits.minimum
168 && maximum < minimum
169 {
170 return Err(io::Error::new(
171 io::ErrorKind::InvalidInput,
172 format!(
173 "a maximum of {maximum} is below this pool's minimum of {minimum}; Win32 \
174 would let the minimum win silently, so the conflict is refused instead"
175 ),
176 ));
177 }
178 // SAFETY: pool is valid for the lifetime of self.
179 crate::trace_call!("SetThreadpoolThreadMaximum", self.pool, maximum, {
180 // SAFETY: pool is valid for the lifetime of self.
181 unsafe { SetThreadpoolThreadMaximum(self.pool, maximum) };
182 });
183 limits.maximum = Some(maximum);
184 Ok(())
185 }
186
187 /// Set the minimum number of threads this pool keeps available.
188 ///
189 /// Raising the minimum makes the pool create threads eagerly, which is what
190 /// guarantees forward progress for callbacks that block on one another.
191 ///
192 /// # Errors
193 ///
194 /// Returns the error from `SetThreadpoolThreadMinimum`, which fails when the
195 /// pool cannot create the requested threads.
196 ///
197 /// Returns [`io::ErrorKind::InvalidInput`] if `minimum` exceeds a maximum
198 /// previously set through [`set_max_threads`](Self::set_max_threads). Win32
199 /// would accept it and run up to `minimum` callbacks concurrently, annulling
200 /// the maximum without reporting anything; see
201 /// [`set_max_threads`](Self::set_max_threads) for the measurements.
202 ///
203 /// # Examples
204 ///
205 /// ```
206 /// use windows_threadpool_sys::pool::ThreadpoolPool;
207 ///
208 /// let pool = ThreadpoolPool::new()?;
209 /// pool.set_max_threads(2)?;
210 /// assert!(pool.set_min_threads(4).is_err());
211 /// pool.set_min_threads(2)?;
212 /// # Ok::<(), std::io::Error>(())
213 /// ```
214 pub fn set_min_threads(&self, minimum: u32) -> io::Result<()> {
215 let mut limits = self.limits.lock().unwrap_or_else(|e| e.into_inner());
216 if let Some(maximum) = limits.maximum
217 && minimum > maximum
218 {
219 return Err(io::Error::new(
220 io::ErrorKind::InvalidInput,
221 format!(
222 "a minimum of {minimum} exceeds this pool's maximum of {maximum}; Win32 \
223 would honour the minimum and annul the maximum silently, so the conflict \
224 is refused instead"
225 ),
226 ));
227 }
228 // SAFETY: pool is valid for the lifetime of self.
229 // This one really does block: it creates threads, and is documented to
230 // fail when it cannot.
231 let ok = crate::trace_call!("SetThreadpoolThreadMinimum", self.pool, minimum, {
232 // SAFETY: pool is valid for the lifetime of self.
233 unsafe { SetThreadpoolThreadMinimum(self.pool, minimum) }
234 });
235 if ok == 0 {
236 return Err(io::Error::last_os_error());
237 }
238 limits.minimum = Some(minimum);
239 Ok(())
240 }
241
242 /// The raw pool value, for storing in a callback environment.
243 pub(crate) fn as_raw(&self) -> PTP_POOL {
244 self.pool
245 }
246}
247
248impl Drop for ThreadpoolPool {
249 fn drop(&mut self) {
250 // SAFETY: pool is valid and owned; the OS releases it once its last
251 // member object is released.
252 crate::trace_call!("CloseThreadpool", self.pool, 0, {
253 // SAFETY: pool is valid and closed exactly once, here.
254 unsafe { CloseThreadpool(self.pool) };
255 });
256 }
257}
258
259/// Make the default process pool create its first worker, and block until one
260/// exists.
261///
262/// # Why this exists
263///
264/// A measured fault in the Windows thread pool needs a pool holding **zero**
265/// threads. A teardown that removes an already-delivered completion packet a few
266/// microseconds after it was queued can sever that port's notification to its
267/// worker factory, after which the pool dispatches nothing -- no wait, no timer,
268/// no I/O completion -- until something submits a work item. The factory reads as
269/// perfectly idle throughout: not paused, not shut down, permitted to create,
270/// nothing failed.
271///
272/// Warming the pool first was measured to prevent it: **0 occurrences in 24000
273/// runs warm, against 66 in 24000 cold**, with a delay-matched cold control still
274/// failing at the cold rate, so it is the worker and not the elapsed time.
275///
276/// # What it does not buy
277///
278/// **This is not a fix and does not make a bad teardown safe.** It removes one
279/// of the fault's preconditions for as long as the pool stays warm, and the pool
280/// stops being warm once it has been idle for its timeout -- 67 seconds on the
281/// machine this was measured on. A long-lived process that goes quiet becomes
282/// cold again, and this function would have to be called again to matter.
283///
284/// It does nothing about teardowns in this crate, which already drain and so
285/// never remove a delivered packet. Its value is against code you do not
286/// control: another library, or a dependency, tearing a wait down badly during
287/// your process's startup.
288///
289/// # It is opt-in, and this crate never calls it for you
290///
291/// Nothing in this crate warms the pool on your behalf -- not `ThreadpoolWait::new`,
292/// not any other constructor. Three reasons, in order of weight:
293///
294/// 1. **It is a process-wide side effect.** A thread created here belongs to the
295/// default pool, which is shared with every other component in the process. A
296/// library that silently adds a resident thread to a pool it does not own has
297/// made a decision that was not its to make.
298/// 2. **This crate's own teardowns do not need it.** They drain, so they never
299/// remove a delivered packet. Calling this automatically would protect against
300/// *other* code while implying that this crate's paths required it.
301/// 3. **It would be a surprising cost in the common case.** Most callers create a
302/// wait and never tear it down badly; charging all of them a thread for a
303/// hazard they do not have is the wrong default.
304///
305/// Whether the protection is worth one resident thread is a judgement about the
306/// process as a whole, which the caller is in a position to make and this crate
307/// is not.
308///
309/// # What was ruled out
310///
311/// Holding the pool warm *permanently* would be better, and is not available:
312/// `SetThreadpoolThreadMinimum` does not accept a null pool, and calling it that
313/// way does not fail -- it **raises `STATUS_INVALID_PARAMETER` and terminates the
314/// process**. The default pool's minimum cannot be set. A private
315/// [`ThreadpoolPool`] can have [`set_min_threads`](ThreadpoolPool::set_min_threads)
316/// applied to it, but that is a different pool.
317///
318/// # It always submits, and does not check first
319///
320/// The obvious optimisation is to read the worker count and skip the work item
321/// when the pool is already warm. It is not done, for two reasons, and the first
322/// is simply that it is slower. Measured on the development machine: prewarming
323/// an already-warm pool costs about 27us, while the query needed to decide to
324/// skip it costs about 204us, because there is no way to ask "how many workers
325/// does the default pool have" without scanning the handle space. The skip would
326/// cost roughly seven times what it saves.
327///
328/// The second reason outlasts the first, and has since become the whole of it.
329/// That query reads a layout Microsoft does not publish, through an undocumented
330/// entry point. A misread returning a plausible non-zero would make this function
331/// skip the warm-up and report success, leaving the caller believing they are
332/// protected when they are not -- the exact failure this exists to prevent. An
333/// unconditional submit cannot be wrong that way.
334///
335/// **This crate no longer contains any way to make that query.** The facility
336/// that did was removed rather than kept behind a feature; see the decision in
337/// the workspace `DESIGN-NOTES.md`. A caller who wants worker counts needs a
338/// mechanism built on documented ground, such as an ETW kernel trace.
339///
340/// # Returns
341///
342/// Whether a worker is confirmed to exist. A callback having run is the proof,
343/// because it ran on one. `false` means the work item could not be created or did
344/// not run within the bound, and the pool should be assumed cold.
345pub fn prewarm_default_pool() -> bool {
346 use std::sync::mpsc;
347 use std::time::Duration;
348
349 /// How long to wait for the warm-up callback. Generous: a healthy pool
350 /// creates its first worker in well under a millisecond, so reaching this
351 /// means something is already wrong.
352 const BOUND: Duration = Duration::from_secs(2);
353
354 let (tx, rx) = mpsc::channel();
355 let tx = std::sync::Mutex::new(tx);
356 let Ok(work) = crate::work::ThreadpoolWork::new(
357 move || {
358 if let Ok(tx) = tx.lock() {
359 let _ = tx.send(());
360 }
361 },
362 None,
363 ) else {
364 return false;
365 };
366 work.submit();
367 let confirmed = rx.recv_timeout(BOUND).is_ok();
368 if !confirmed {
369 // Cancel before draining, on this path only.
370 //
371 // Reaching the timeout means the callback was never dispatched, which
372 // is the condition this function exists to report. Draining then waits
373 // for that same undispatched callback **with no deadline at all**, so
374 // the bounded check above would be followed by an unbounded one and the
375 // `false` this is supposed to return would never arrive. The bound
376 // would be decoration.
377 //
378 // Cancelling discards the queued invocation and waits only for one
379 // already executing, and only on this object: the warm-up callback does
380 // nothing but send on a channel, so it cannot block, and another
381 // object's stuck callback is not this call's to wait for.
382 // This also discharges the obligation: `cancel_pending` returns with
383 // nothing queued and nothing executing, and settles on that basis. The
384 // drain below would therefore be a second native synchronisation that
385 // waits for a state already reached.
386 work.cancel_pending();
387 } else {
388 // Discharged here rather than left to the drop below. The drop would
389 // drain anyway, so this adds no blocking -- but this function is the
390 // crate's own use of its own protocol, and leaving the obligation
391 // undischarged makes it a reported violation like any other.
392 //
393 // Needed only on this path, and that asymmetry is deliberate rather
394 // than an oversight: a dispatch does not settle a work object's
395 // obligation, because a work object can be submitted again and the
396 // drain stays the caller's regardless of what has already run. So the
397 // confirmed path has a live obligation even though its callback
398 // demonstrably ran, while the timeout path above has already cleared
399 // one.
400 work.stop_and_drain();
401 }
402 confirmed
403}
404
405#[cfg(test)]
406mod tests;