Skip to main content

ThreadpoolWait

Struct ThreadpoolWait 

Source
pub struct ThreadpoolWait { /* private fields */ }
Expand description

An owned thread-pool wait object bound to one waitable handle.

The object owns the handle, so the handle cannot be closed while a wait is pending. A newly created wait is idle; arm it with ThreadpoolWait::arm, and rearm from inside the callback with WaitActivation::rearm.

Drop disarms before draining callbacks, then closes the object and only afterwards releases the callback context and the handle.

Unlike ThreadpoolTimer, the callback can run concurrently with itself: a wait’s re-arm takes effect immediately, so re-arming while the handle is still signalled queues the next activation before the current callback returns. See WaitActivation::rearm for the measurements and the two ways to avoid it.

§Examples

Watch an event once. The wait takes ownership of the handle it is given, so keep a clone of the Event – a second handle to the same event – to signal it through:

use std::sync::mpsc;
use win_sync_sys::{Event, ResetMode};
use windows_threadpool_sys::wait::{ThreadpoolWait, WaitResult};

let event = Event::new(ResetMode::Manual, false)?;
let signal = event.try_clone()?;

let (tx, rx) = mpsc::channel();
let sender = std::sync::Mutex::new(tx);
let wait = ThreadpoolWait::new(event.into(), move |activation| {
    let _ = sender.lock().expect("send").send(activation.result());
}, None)?;

wait.arm(None);
signal.set()?;

assert_eq!(rx.recv().expect("activation"), WaitResult::Signalled);

Keep watching across activations by rearming from inside the callback, which is what the SDK requires – an activation consumes the arming:

use win_sync_sys::{Event, ResetMode};
use windows_threadpool_sys::wait::ThreadpoolWait;

let event = Event::new(ResetMode::Auto, false)?;
let signal = event.try_clone()?;

let seen = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&seen);
let wait = ThreadpoolWait::new(event.into(), move |activation| {
    counter.fetch_add(1, Ordering::SeqCst);
    activation.rearm(None);
}, None)?;

wait.arm(None);
for _ in 0..3 {
    signal.set()?;
    std::thread::sleep(std::time::Duration::from_millis(5));
}

// Disarms and drains in one step, discharging the drain this wait owes
// rather than leaving the blocking teardown to `Drop`.
wait.stop_and_drain();
assert!(seen.load(Ordering::SeqCst) >= 1);

Implementations§

Source§

impl ThreadpoolWait

Source

pub fn new<F>( handle: WaitableHandle, callback: F, env: Option<&mut CallbackEnviron<'_>>, ) -> Result<Self>
where F: Fn(&WaitActivation<'_>) + Send + Sync + 'static,

Create an idle wait watching handle.

The object takes ownership of the handle and closes it on drop, which is what guarantees the handle outlives any pending wait.

Pass Some(env) to select a private pool or callback priority; None uses the process-default pool with default priority.

The callback runs on a shared, process-managed pool thread, must restore any thread state it changes, and must not terminate its thread. It must not panic: a panic unwinds to the extern "system" trampoline and aborts the process.

Taking a WaitableHandle rather than a bare handle is what keeps this constructor safe: the thread pool does not support every waitable object, and a mutex handle in particular is undefined rather than an error.

§Errors

Returns the error from CreateThreadpoolWait.

Source

pub fn handle(&self) -> BorrowedHandle<'_>

Borrow the watched handle, for signalling or inspecting it.

Source

pub fn arm(&self, timeout: Option<Duration>)

Arm the wait, so the next signal or timeout runs the callback once.

timeout of None waits indefinitely. Arming replaces any previous arming rather than adding to it, and an activation consumes the arming – rearm from inside the callback with WaitActivation::rearm to keep watching.

§Arm before you signal

SetThreadpoolWait documents that “you must re-register the event with the wait object before signaling it each time to trigger the wait callback”. Signal a handle that is not currently armed – including in the window between constructing a ThreadpoolWait and this call – and the callback is not guaranteed to run for that signal.

An auto-reset event makes a dropped signal permanent rather than merely late, because the signal is consumed and there is nothing left for a subsequent arming to observe. If the handle is only ever signalled once, as a wakeup for state that is already present, that lost signal is the last one the waiter will ever get.

Every example in this module arms first for that reason; so does every caller in this workspace. A caller who cannot control the ordering should watch a manual-reset event kept in agreement with the state it reports, which is level-triggered and so has no signal to lose.

Source

pub fn disarm(&self)

Stop watching.

New activations stop being queued, but a callback already queued still runs; use try_cancel_pending, or try_cancel_pending_no_heal_tracking in a build without the self-heal feature, to drop those as well.

Source

pub fn wait(&self)

Let every queued callback run, and block until none is executing.

This does not leave a self-re-arming wait idle: a callback running during this call can rearm before it returns, so the object is watching again when this returns. Use stop_and_drain to reach quiescence.

Source

pub fn try_cancel_pending(&self)

Drop callbacks that have not started, then wait for any executing one.

Like wait, this does not by itself leave a self-re-arming wait idle: it does not suppress the re-arm of a callback that is already running. Use stop_and_drain when the wait must actually be quiescent afterwards.

§This brings a process-wide hazard forward; it does not create it

Cancelling asks the kernel to remove a completion packet that may already have been delivered to the pool’s completion port (IopCancelWaitCompletionPacket with RemoveSignaledPacket set). Measured on this workspace’s reproducer, a removal that lands a few microseconds after the packet was queued, on a port whose factory has no threads yet, can sever the notification from that port to the worker factory.

The scope of that is one pool – but it is the whole of that pool, not this wait and not waits as a kind. Measured during such a stall: already armed waits, a freshly armed and signalled wait, a fresh timer, and a completed overlapped read all fail to dispatch. A second pool in the same process is unaffected. Since the pool normally affected is the default one, the practical reach is every component in the process that did not explicitly create its own – including code that has nothing to do with the caller.

Submitting a work item recovers it, because that reaches the factory by a route the broken notification is not on. Nothing else observed does.

But this method is not the only way to reach that, and on the measured evidence it is not even the usual one. CloseThreadpoolWait performs the same removal, through the same kernel routine with the same flag, whenever it finds a packet still outstanding – so a teardown that never calls this method reaches the hazard anyway. In the reproducer, dropping the cancel entirely changed nothing measurable: 22 failures in 20004 without it against 16 with it.

What this method does is perform the removal earlier, which removes any chance for the callback to run first.

The fix is therefore not to avoid this method but to leave nothing to remove: wait and stop_and_drain let the queued callback run, which clears the association, after which the close has nothing to take. Draining cannot reach the removal primitive on any path. Prefer them – but note that doing so is what makes the subsequent close safe, not merely this call. This crate repairs the pool afterwards, which is what makes this safe to offer – with one exception, stated because a guarantee with an unstated hole is worse than one that names it.

If this object’s pool could not be registered for repair when the object was created, which happens only when creating the repair work item failed, this call registers it here rather than giving up. Only when that allocation fails too is the cancellation performed with nothing to repair it, and that case records cancel-untracked on the heal target – naming the pool – and panics under the fail-fast feature. See README-FEATURE-self-heal.md.

§Availability

Requires the self-heal feature, which is on by default. Without it this method does not exist and a call to it fails to compile, naming try_cancel_pending_no_heal_tracking as what to reach for instead. That is the designed behaviour, not an oversight: a guarantee you were relying on has been removed, and a compile error is the only way you find that out.

Source

pub unsafe fn try_cancel_pending_no_heal_tracking(&self)

try_cancel_pending without the repair.

Not a link, deliberately: the method it would name does not exist in a build with self-heal off, and this one does, so the link would dangle in exactly the configuration this method exists for.

Always present, including in builds with self-heal off, which is the point: it is the method that still exists when the gated one does not, and its signature says what the caller takes on.

§Safety

The caller must ensure the pool is repaired. Submitting any work item to it does so; so does knowing the pool is kept live by something else. Leaving it unrepaired can stop the pool dispatching – which, for the process-default pool, reaches every component in the process, including code with no connection to this call.

This is not a memory-safety obligation, and the keyword is not claiming one. It is here because the obligation is statable and dischargeable by the caller, which is what unsafe marks; see the self-heal decision.

Source

pub fn stop_and_drain(&self)

Stop watching and block until the wait is idle, leaving it reusable.

This exists because neither disarm nor try_cancel_pending can stop a self-re-arming wait on its own: a callback already running can call WaitActivation::rearm after a disarm from outside has taken effect. This suppresses re-arming for the duration of the call, using the same mechanism Drop uses, and lifts the suppression before returning so the wait can be armed again.

§What this guarantees

On return, provided no other thread arms the wait during the call:

  • no callback is queued or executing, and
  • the object is not watching – a re-arm requested by a callback that ran during the call is discarded rather than deferred.

A queued callback runs; it is not discarded. That is the difference between this and try_cancel_pending, and it is why this can block for as long as the callback takes. See the teardown-drains decision for why finishing the work is preferred to abandoning it.

§What it does not

A concurrent arm from another thread is not excluded. ThreadpoolWait is Sync and arm takes &self, so it does not pass through the suppression this uses, and nothing in this crate orders such a call against this one. A caller needing the wait to be provably idle must ensure nothing else arms it for the duration, by owning it exclusively or serializing access to it.

Calling this from inside the wait’s own callback would deadlock, because it waits for that callback to finish.

Trait Implementations§

Source§

impl Drop for ThreadpoolWait

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl Send for ThreadpoolWait

Source§

impl Sync for ThreadpoolWait

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.