Skip to main content

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;