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
impl ThreadpoolWait
Sourcepub fn new<F>(
handle: WaitableHandle,
callback: F,
env: Option<&mut CallbackEnviron<'_>>,
) -> Result<Self>
pub fn new<F>( handle: WaitableHandle, callback: F, env: Option<&mut CallbackEnviron<'_>>, ) -> Result<Self>
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.
Sourcepub fn handle(&self) -> BorrowedHandle<'_>
pub fn handle(&self) -> BorrowedHandle<'_>
Borrow the watched handle, for signalling or inspecting it.
Sourcepub fn arm(&self, timeout: Option<Duration>)
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.
Sourcepub fn disarm(&self)
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.
Sourcepub fn wait(&self)
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.
Sourcepub fn try_cancel_pending(&self)
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.
Sourcepub unsafe fn try_cancel_pending_no_heal_tracking(&self)
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.
Sourcepub fn stop_and_drain(&self)
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.