windows_threadpool_sys/cleanup_group.rs
1// Copyright (c) 2026 Mike Grier
2//! Cleanup groups: releasing many thread-pool objects in one step.
3//!
4//! A cleanup group tears down every object created into it with a single
5//! `CloseThreadpoolCleanupGroupMembers`, which waits for executing callbacks and
6//! (optionally) cancels those that have not started. That is the SDK's answer to
7//! shutting down a subsystem without tracking each object individually.
8//!
9//! # Why members are created *by* the group
10//!
11//! Releasing members is bulk and irreversible: afterwards a member must not be
12//! used or closed again, and only then is its heap callback context safe to
13//! free. An individually-owned object cannot know when that has happened, so the
14//! group owns both the members and their contexts.
15//!
16//! That ownership is expressed in the types. Members borrow the group, and
17//! [`CleanupGroup::close_members`] takes `&mut self`, so the borrow checker
18//! rejects any use of a member after the group has released it:
19//!
20//! ```compile_fail
21//! # use windows_threadpool_sys::cleanup_group::CleanupGroup;
22//! let mut group = CleanupGroup::new().expect("create group");
23//! let work = group.create_work(|| {}, None).expect("create work");
24//! group.close_members(false);
25//! work.submit(); // error: `group` is mutably borrowed above
26//! ```
27//!
28//! # Thread-pool I/O is deliberately excluded
29//!
30//! There is no `create_io`. A `TP_IO` object must not be closed while any
31//! overlapped operation is outstanding, because the kernel still owns that
32//! operation's storage -- and a cleanup group's bulk release has no way to
33//! satisfy that precondition for its members. [`crate::io::ThreadpoolIo`]
34//! therefore stays individually owned, where its `Drop` can cancel, drain, and
35//! only then close. Grouping it would trade a guarantee for a convenience.
36
37use core::ffi::c_void;
38use std::io;
39use std::marker::PhantomData;
40use std::os::windows::io::BorrowedHandle;
41use std::ptr;
42use std::sync::Mutex;
43use std::time::{Duration, SystemTime};
44
45use windows_sys::Win32::Foundation::{FALSE, TRUE};
46use windows_sys::Win32::System::Threading::{
47 CloseThreadpoolCleanupGroup, CloseThreadpoolCleanupGroupMembers, CreateThreadpoolCleanupGroup,
48 IsThreadpoolTimerSet, PTP_CLEANUP_GROUP, PTP_TIMER, PTP_WAIT, PTP_WORK, SubmitThreadpoolWork,
49 WaitForThreadpoolTimerCallbacks, WaitForThreadpoolWaitCallbacks,
50 WaitForThreadpoolWorkCallbacks,
51};
52
53use crate::callback_env::CallbackEnviron;
54use crate::timer::{
55 PeriodicTick, ThreadpoolPeriodicTimer, ThreadpoolTimer, TimerFiring, absolute_filetime,
56 arm_raw, disarm_raw, millis_u32, relative_filetime,
57};
58use crate::wait::{ThreadpoolWait, WaitActivation, WaitTarget, WaitableHandle};
59use crate::work::ThreadpoolWork;
60
61/// A heap allocation the group frees once its members have been released.
62///
63/// Resources are type-erased because one group holds members of several kinds;
64/// each entry carries the function that knows how to free it, and the function
65/// that prepares its member for the bulk release.
66struct OwnedResource {
67 ptr: *mut c_void,
68 /// Suppress the member's deferred re-arm and disarm it before
69 /// `CloseThreadpoolCleanupGroupMembers` runs. A no-op for kinds with no
70 /// callback-driven re-arm (work, periodic timers, watched handles).
71 prepare_shutdown: unsafe fn(*mut c_void),
72 free: unsafe fn(*mut c_void),
73}
74
75// SAFETY: each pointer is a `Box` the group exclusively owns and frees exactly
76// once, after the pool has released every member that could reach it.
77unsafe impl Send for OwnedResource {}
78
79/// Free a boxed value the group owns directly, rather than a callback context.
80///
81/// SAFETY: `ptr` must be a `Box<T>` reclaimed exactly once.
82unsafe fn free_boxed<T>(ptr: *mut c_void) {
83 // SAFETY: forwarded from this function's own contract.
84 drop(unsafe { Box::from_raw(ptr.cast::<T>()) });
85}
86
87/// A shutdown preparation for a member with no callback-driven re-arm to
88/// suppress: work objects, periodic timers, and watched handles.
89///
90/// `CloseThreadpoolCleanupGroupMembers` already disarms and cancels these; only
91/// a one-shot timer or a wait can re-arm itself from inside a callback, so only
92/// those need the real preparation.
93fn prepare_shutdown_noop(_ptr: *mut c_void) {}
94
95/// An owned thread-pool cleanup group.
96///
97/// Create members with [`CleanupGroup::create_work`],
98/// [`CleanupGroup::create_timer`], [`CleanupGroup::create_periodic_timer`], and
99/// [`CleanupGroup::create_wait`], then release them all with
100/// [`CleanupGroup::close_members`]. `Drop` releases any members that are still
101/// open, so forgetting to call `close_members` is safe -- it only gives up
102/// control over *when* the teardown blocks.
103///
104/// # Examples
105///
106/// ```
107/// use std::sync::Arc;
108/// use std::sync::atomic::{AtomicUsize, Ordering};
109/// use std::time::Duration;
110/// use windows_threadpool_sys::cleanup_group::CleanupGroup;
111///
112/// let count = Arc::new(AtomicUsize::new(0));
113/// let work_counter = Arc::clone(&count);
114/// let timer_counter = Arc::clone(&count);
115///
116/// let mut group = CleanupGroup::new()?;
117/// {
118/// let work = group.create_work(move || {
119/// work_counter.fetch_add(1, Ordering::SeqCst);
120/// }, None)?;
121/// let timer = group.create_timer(move |_firing| {
122/// timer_counter.fetch_add(1, Ordering::SeqCst);
123/// }, None)?;
124///
125/// work.submit();
126/// timer.set_after(Duration::from_millis(1));
127///
128/// // Wait for the work to have run and the timer to have fired. Note that
129/// // `timer.wait()` would not do: it waits for callbacks the pool has
130/// // already queued, and a timer that has not expired yet has none.
131/// while count.load(Ordering::SeqCst) < 2 {
132/// std::thread::yield_now();
133/// }
134/// }
135///
136/// // One call tears down every member of the group.
137/// group.close_members(false);
138/// assert_eq!(count.load(Ordering::SeqCst), 2);
139/// # Ok::<(), std::io::Error>(())
140/// ```
141pub struct CleanupGroup {
142 group: PTP_CLEANUP_GROUP,
143 /// Contexts and handles owned on behalf of members, freed after release.
144 ///
145 /// This is the only record of what is outstanding, and it is deliberately
146 /// not paired with a "already released" flag. Such a flag would latch: the
147 /// `create_*` methods take `&self`, so members can be created after a
148 /// release returns, and a latched release would then skip them -- leaking
149 /// their contexts and closing the group with live members.
150 resources: Mutex<Vec<OwnedResource>>,
151}
152
153// SAFETY: PTP_CLEANUP_GROUP is a kernel-managed object usable from any thread,
154// and the resource list is mutex-guarded.
155unsafe impl Send for CleanupGroup {}
156unsafe impl Sync for CleanupGroup {}
157
158impl CleanupGroup {
159 /// Create an empty cleanup group.
160 ///
161 /// # Errors
162 ///
163 /// Returns the error from `CreateThreadpoolCleanupGroup`.
164 pub fn new() -> io::Result<Self> {
165 // SAFETY: the call takes no inputs.
166 let group = unsafe { CreateThreadpoolCleanupGroup() };
167 if group == 0 {
168 return Err(io::Error::last_os_error());
169 }
170 Ok(Self {
171 group,
172 resources: Mutex::new(Vec::new()),
173 })
174 }
175
176 /// Build the environment a member is created with, layering this group on
177 /// top of whatever pool and priority the caller chose.
178 ///
179 /// The caller's environment is copied rather than mutated, so passing one
180 /// environment to several groups -- or reusing it for a non-member object --
181 /// behaves as written.
182 fn member_environment(&self, env: Option<&CallbackEnviron<'_>>) -> CallbackEnviron<'_> {
183 let mut member_env = match env {
184 Some(env) => CallbackEnviron::from_inner(*env.as_inner()),
185 None => CallbackEnviron::new(),
186 };
187 // SAFETY: `self.group` is live for at least as long as the member being
188 // created, because the member borrows this group, and the member is
189 // never closed individually -- `close_members` releases it.
190 unsafe { member_env.set_cleanup_group(self.group, None) };
191 member_env
192 }
193
194 fn adopt(&self, resource: OwnedResource) {
195 self.resources
196 .lock()
197 .unwrap_or_else(|poison| poison.into_inner())
198 .push(resource);
199 }
200
201 /// Create a work object owned by this group.
202 ///
203 /// Equivalent to [`ThreadpoolWork::new`], except that the returned member is
204 /// released by [`CleanupGroup::close_members`] rather than by its own drop.
205 ///
206 /// # Errors
207 ///
208 /// Returns the error from `CreateThreadpoolWork`.
209 pub fn create_work<F>(
210 &self,
211 callback: F,
212 env: Option<&CallbackEnviron>,
213 ) -> io::Result<WorkMember<'_>>
214 where
215 F: Fn() + Send + Sync + 'static,
216 {
217 let mut member_env = self.member_environment(env);
218 let work = ThreadpoolWork::new(callback, Some(&mut member_env))?;
219 let (handle, context) = work.into_parts();
220 self.adopt(OwnedResource {
221 ptr: context,
222 prepare_shutdown: prepare_shutdown_noop,
223 free: ThreadpoolWork::drop_context,
224 });
225 Ok(WorkMember {
226 handle,
227 _group: PhantomData,
228 })
229 }
230
231 /// Create a one-shot timer owned by this group.
232 ///
233 /// Equivalent to [`ThreadpoolTimer::new`].
234 ///
235 /// # Errors
236 ///
237 /// Returns the error from `CreateThreadpoolTimer`.
238 pub fn create_timer<F>(
239 &self,
240 callback: F,
241 env: Option<&CallbackEnviron>,
242 ) -> io::Result<TimerMember<'_>>
243 where
244 F: Fn(&TimerFiring<'_>) + Send + Sync + 'static,
245 {
246 let mut member_env = self.member_environment(env);
247 let timer = ThreadpoolTimer::new(callback, Some(&mut member_env))?;
248 let (handle, context) = timer.into_parts();
249 self.adopt(OwnedResource {
250 ptr: context,
251 prepare_shutdown: ThreadpoolTimer::prepare_shutdown,
252 free: ThreadpoolTimer::drop_context,
253 });
254 Ok(TimerMember {
255 handle,
256 _group: PhantomData,
257 })
258 }
259
260 /// Create a periodic timer owned by this group.
261 ///
262 /// Equivalent to [`ThreadpoolPeriodicTimer::new`], including that its ticks
263 /// may overlap one another.
264 ///
265 /// # Errors
266 ///
267 /// Returns [`io::ErrorKind::InvalidInput`] if `period` is outside
268 /// [`ThreadpoolPeriodicTimer::MIN_PERIOD`]..=[`ThreadpoolPeriodicTimer::MAX_PERIOD`]
269 /// or is not a whole number of milliseconds, or the error from
270 /// `CreateThreadpoolTimer`.
271 pub fn create_periodic_timer<F>(
272 &self,
273 period: Duration,
274 callback: F,
275 env: Option<&CallbackEnviron>,
276 ) -> io::Result<PeriodicTimerMember<'_>>
277 where
278 F: Fn(&PeriodicTick<'_>) + Send + Sync + 'static,
279 {
280 let mut member_env = self.member_environment(env);
281 let timer = ThreadpoolPeriodicTimer::new(period, callback, Some(&mut member_env))?;
282 let (handle, context, period) = timer.into_parts();
283 self.adopt(OwnedResource {
284 ptr: context,
285 prepare_shutdown: prepare_shutdown_noop,
286 free: ThreadpoolPeriodicTimer::drop_context,
287 });
288 Ok(PeriodicTimerMember {
289 handle,
290 period,
291 _group: PhantomData,
292 })
293 }
294
295 /// Create a wait object owned by this group, watching `handle`.
296 ///
297 /// The group takes ownership of the handle as well as the object, because
298 /// the pool may still be watching it until the members are released.
299 ///
300 /// Like [`ThreadpoolWait::new`], this takes a [`WaitableHandle`] rather than
301 /// a bare handle, so the group path cannot reach the unsupported wait
302 /// targets that the individually-owned path rejects.
303 ///
304 /// # Errors
305 ///
306 /// Returns the error from `CreateThreadpoolWait`.
307 pub fn create_wait<F>(
308 &self,
309 handle: WaitableHandle,
310 callback: F,
311 env: Option<&CallbackEnviron<'_>>,
312 ) -> io::Result<WaitMember<'_>>
313 where
314 F: Fn(&WaitActivation<'_>) + Send + Sync + 'static,
315 {
316 let mut member_env = self.member_environment(env);
317 let wait = ThreadpoolWait::new(handle, callback, Some(&mut member_env))?;
318 let (raw, context, target) = wait.into_parts();
319 self.adopt(OwnedResource {
320 ptr: context,
321 prepare_shutdown: ThreadpoolWait::prepare_shutdown,
322 free: ThreadpoolWait::drop_context,
323 });
324 // The target outlives the member for the same reason the context does.
325 // Freeing the box runs `WaitTarget`'s drop, which closes the handle with
326 // whichever routine it was built with -- `CloseHandle` for the default
327 // path, the caller's for a custom-close target.
328 let target = Box::into_raw(Box::new(target));
329 self.adopt(OwnedResource {
330 ptr: target.cast(),
331 prepare_shutdown: prepare_shutdown_noop,
332 free: free_boxed::<WaitTarget>,
333 });
334 Ok(WaitMember {
335 handle: raw,
336 watched: target,
337 _group: PhantomData,
338 })
339 }
340
341 /// Release every member of this group.
342 ///
343 /// Waits for executing callbacks to finish. When `cancel_pending` is true,
344 /// callbacks that have not started are dropped instead of run; when false,
345 /// they run first.
346 ///
347 /// Taking `&mut self` is what makes members unusable afterwards: they borrow
348 /// the group, so the compiler rejects any later use of one. Calling this
349 /// twice is harmless -- the second call finds no members.
350 ///
351 /// The group remains usable afterwards. New members may be created on it,
352 /// and they are released by the next call or by `Drop`, exactly as the first
353 /// batch was.
354 pub fn close_members(&mut self, cancel_pending: bool) {
355 self.release_members(cancel_pending);
356 }
357
358 /// The number of contexts and handles the group is holding for its members.
359 ///
360 /// Zero once the members have been released.
361 #[must_use]
362 pub fn owned_resources(&self) -> usize {
363 self.resources
364 .lock()
365 .unwrap_or_else(|poison| poison.into_inner())
366 .len()
367 }
368
369 /// Release whatever members exist right now.
370 ///
371 /// Runs in full every time rather than latching after the first call. The
372 /// native release is idempotent -- with no members it does nothing -- and
373 /// running unconditionally is what makes a group usable again afterwards:
374 /// members created after an earlier release are released by the next one,
375 /// instead of being skipped and leaked.
376 fn release_members(&mut self, cancel_pending: bool) {
377 // Close the door on any deferred re-arm before the bulk release.
378 // `CloseThreadpoolCleanupGroupMembers` waits for executing callbacks but
379 // does not stop one from re-arming: a one-shot timer or wait whose
380 // callback is running can request a re-arm the trampoline applies after
381 // it returns, which would re-arm an object the release is tearing down
382 // and then free its context under a freshly queued callback. Suppressing
383 // and disarming each member first mirrors what each object's own `Drop`
384 // does. The lock is dropped before the release, which blocks.
385 {
386 let resources = self
387 .resources
388 .lock()
389 .unwrap_or_else(|poison| poison.into_inner());
390 for resource in resources.iter() {
391 // SAFETY: the members are still live and unreleased; each hook
392 // matches the context kind this resource holds and only
393 // suppresses/disarms that one object.
394 unsafe { (resource.prepare_shutdown)(resource.ptr) };
395 }
396 }
397
398 // SAFETY: the group is live. This waits for executing callbacks and
399 // releases every member, so afterwards nothing can reach the contexts.
400 unsafe {
401 CloseThreadpoolCleanupGroupMembers(
402 self.group,
403 if cancel_pending { TRUE } else { FALSE },
404 ptr::null_mut(),
405 );
406 }
407
408 let resources = std::mem::take(
409 &mut *self
410 .resources
411 .lock()
412 .unwrap_or_else(|poison| poison.into_inner()),
413 );
414 for resource in resources {
415 // SAFETY: every member has been released, so no callback can still
416 // reach this allocation; each is freed exactly once here.
417 unsafe { (resource.free)(resource.ptr) };
418 }
419 }
420}
421
422impl Drop for CleanupGroup {
423 fn drop(&mut self) {
424 // Let queued callbacks run, matching the default of `close_members`.
425 self.release_members(false);
426 // SAFETY: the members are released, so the group can be closed.
427 unsafe { CloseThreadpoolCleanupGroup(self.group) };
428 }
429}
430
431impl std::fmt::Debug for CleanupGroup {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 f.debug_struct("CleanupGroup")
434 .field("owned_resources", &self.owned_resources())
435 .finish_non_exhaustive()
436 }
437}
438
439/// A work object owned by a [`CleanupGroup`].
440///
441/// Behaves like [`ThreadpoolWork`] but is released by the group rather than by
442/// its own drop.
443#[derive(Debug)]
444pub struct WorkMember<'group> {
445 handle: PTP_WORK,
446 _group: PhantomData<&'group CleanupGroup>,
447}
448
449impl WorkMember<'_> {
450 /// Queue one invocation of the callback.
451 pub fn submit(&self) {
452 // SAFETY: the handle is live until the group releases its members,
453 // which the borrow on `_group` prevents from happening first.
454 unsafe { SubmitThreadpoolWork(self.handle) };
455 }
456
457 /// Block until all queued and in-progress invocations have completed.
458 pub fn wait(&self) {
459 // SAFETY: as above.
460 unsafe { WaitForThreadpoolWorkCallbacks(self.handle, FALSE) };
461 }
462
463 /// Cancel invocations that have not started, then wait for those that have.
464 pub fn cancel_pending(&self) {
465 // SAFETY: as above.
466 unsafe { WaitForThreadpoolWorkCallbacks(self.handle, TRUE) };
467 }
468}
469
470/// A one-shot timer owned by a [`CleanupGroup`].
471///
472/// Behaves like [`ThreadpoolTimer`] but is released by the group rather than by
473/// its own drop.
474#[derive(Debug)]
475pub struct TimerMember<'group> {
476 handle: PTP_TIMER,
477 _group: PhantomData<&'group CleanupGroup>,
478}
479
480impl TimerMember<'_> {
481 /// Fire once, `delay` from now.
482 pub fn set_after(&self, delay: Duration) {
483 // SAFETY: the handle is live until the group releases its members.
484 unsafe { arm_raw(self.handle, relative_filetime(delay), 0, 0) };
485 }
486
487 /// Fire once at the wall-clock instant `when`.
488 pub fn set_at(&self, when: SystemTime) {
489 // SAFETY: as above.
490 unsafe { arm_raw(self.handle, absolute_filetime(when), 0, 0) };
491 }
492
493 /// Stop the timer.
494 pub fn disarm(&self) {
495 // SAFETY: as above.
496 unsafe { disarm_raw(self.handle) };
497 }
498
499 /// Whether the timer currently has a due time.
500 ///
501 /// As with [`ThreadpoolTimer::is_set`], expiring does not clear the due
502 /// time; only disarming does.
503 #[must_use]
504 pub fn is_set(&self) -> bool {
505 // SAFETY: as above.
506 unsafe { IsThreadpoolTimerSet(self.handle) != 0 }
507 }
508
509 /// Block until all queued and executing callbacks have completed.
510 pub fn wait(&self) {
511 // SAFETY: as above.
512 unsafe { WaitForThreadpoolTimerCallbacks(self.handle, FALSE) };
513 }
514
515 /// Cancel callbacks that have not started, then wait for those that have.
516 pub fn cancel_pending(&self) {
517 // SAFETY: as above.
518 unsafe { WaitForThreadpoolTimerCallbacks(self.handle, TRUE) };
519 }
520}
521
522/// A periodic timer owned by a [`CleanupGroup`].
523///
524/// Behaves like [`ThreadpoolPeriodicTimer`] -- including that its ticks may run
525/// concurrently with one another -- but is released by the group rather than by
526/// its own drop.
527#[derive(Debug)]
528pub struct PeriodicTimerMember<'group> {
529 handle: PTP_TIMER,
530 period: Duration,
531 _group: PhantomData<&'group CleanupGroup>,
532}
533
534impl PeriodicTimerMember<'_> {
535 /// The period this timer ticks on.
536 #[must_use]
537 pub fn period(&self) -> Duration {
538 self.period
539 }
540
541 /// Start ticking, with the first tick one period from now.
542 pub fn start(&self) {
543 self.start_after(self.period);
544 }
545
546 /// Start ticking, with the first tick `first_delay` from now.
547 pub fn start_after(&self, first_delay: Duration) {
548 // SAFETY: the handle is live until the group releases its members.
549 unsafe {
550 arm_raw(
551 self.handle,
552 relative_filetime(first_delay),
553 millis_u32(self.period),
554 0,
555 );
556 }
557 }
558
559 /// Stop the timer.
560 pub fn stop(&self) {
561 // SAFETY: as above.
562 unsafe { disarm_raw(self.handle) };
563 }
564
565 /// Whether the timer is currently started.
566 #[must_use]
567 pub fn is_running(&self) -> bool {
568 // SAFETY: as above.
569 unsafe { IsThreadpoolTimerSet(self.handle) != 0 }
570 }
571
572 /// Block until all queued and executing ticks have completed.
573 pub fn wait(&self) {
574 // SAFETY: as above.
575 unsafe { WaitForThreadpoolTimerCallbacks(self.handle, FALSE) };
576 }
577
578 /// Stop the timer and wait until no tick is queued or executing.
579 ///
580 /// As with [`ThreadpoolPeriodicTimer::stop_and_drain`], this holds provided
581 /// no other thread starts the member during the call: the `start*` methods
582 /// take `&self`, so a start landing between the stop and the drain would
583 /// leave a schedule installed on return.
584 pub fn stop_and_drain(&self) {
585 self.stop();
586 // SAFETY: as above.
587 unsafe { WaitForThreadpoolTimerCallbacks(self.handle, TRUE) };
588 }
589}
590
591/// A wait object owned by a [`CleanupGroup`].
592///
593/// Behaves like [`ThreadpoolWait`] but is released by the group rather than by
594/// its own drop, and the watched handle is owned by the group.
595#[derive(Debug)]
596pub struct WaitMember<'group> {
597 handle: PTP_WAIT,
598 watched: *mut WaitTarget,
599 _group: PhantomData<&'group CleanupGroup>,
600}
601
602// SAFETY: both pointers refer to state the group owns and outlives this member;
603// the member only reads them and passes them to thread-safe pool APIs.
604unsafe impl Send for WaitMember<'_> {}
605unsafe impl Sync for WaitMember<'_> {}
606
607impl WaitMember<'_> {
608 /// Borrow the watched handle, for signalling or inspecting it.
609 #[must_use]
610 pub fn handle(&self) -> BorrowedHandle<'_> {
611 // SAFETY: the target is owned by the group, which outlives this member.
612 unsafe { (*self.watched).borrow() }
613 }
614
615 /// Arm the wait, so the next signal or timeout runs the callback once.
616 pub fn arm(&self, timeout: Option<Duration>) {
617 // SAFETY: the object and handle are live until the group releases its
618 // members, which the borrow on `_group` prevents from happening first.
619 unsafe { crate::wait::arm_member(self.handle, &*self.watched, timeout) };
620 }
621
622 /// Stop watching.
623 pub fn disarm(&self) {
624 // SAFETY: as above.
625 unsafe { crate::wait::disarm_raw(self.handle) };
626 }
627
628 /// Block until all queued and executing callbacks have completed.
629 pub fn wait(&self) {
630 // SAFETY: as above.
631 unsafe { WaitForThreadpoolWaitCallbacks(self.handle, FALSE) };
632 }
633
634 /// Cancel callbacks that have not started, then wait for those that have.
635 pub fn cancel_pending(&self) {
636 // SAFETY: as above.
637 unsafe { WaitForThreadpoolWaitCallbacks(self.handle, TRUE) };
638 }
639}
640
641#[cfg(test)]
642mod tests;