docs(worker): record the SemaphoreSlim/STA dispatch invariant + single-waiter contract; harden fake
This commit is contained in:
@@ -68,6 +68,11 @@ public interface IWorkerRuntimeSession : IDisposable
|
||||
/// no default interface members, so every implementation supplies it. The wait's outcome is
|
||||
/// not surfaced: the caller re-drains and re-checks <see cref="DrainFault"/> after every
|
||||
/// wait, because the signal is coalesced and the timeout is a ceiling, not a poll period.
|
||||
/// <para>
|
||||
/// Precondition: <b>at most one waiter</b>. The implementation's wake signal carries a
|
||||
/// single permit, which is sufficient only because a session has exactly one event
|
||||
/// drain loop; a second concurrent waiter would degrade to the fallback timeout.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="timeout">Maximum time to wait before the wait completes unsignalled.</param>
|
||||
/// <param name="cancellationToken">Token cancelling the wait.</param>
|
||||
|
||||
@@ -75,6 +75,13 @@ public sealed class MxAccessEventQueue
|
||||
// waiter, no wait handle is ever materialized (nothing touches AvailableWaitHandle), and making
|
||||
// the queue IDisposable would only add a lifecycle race between the STA's Enqueue and the
|
||||
// drain loop's wait. WorkerFrameWriter's _writeLock is held the same way.
|
||||
//
|
||||
// It must STAY a SemaphoreSlim: on .NET Framework 4.8, Release() hands an async waiter to the
|
||||
// thread pool (the waiter is a TaskNode : IThreadPoolWorkItem) rather than completing it
|
||||
// inline, so the STA thread's Enqueue never runs the drain loop's continuation. Swapping this
|
||||
// for a TaskCompletionSource — a refactor that looks cosmetic — would complete the waiter
|
||||
// inline on the caller and put the drain loop's pipe write on the apartment thread, which is a
|
||||
// real STA/COM reentrancy hazard (and stalls the message pump behind the write).
|
||||
private readonly SemaphoreSlim eventSignal = new SemaphoreSlim(initialCount: 0, maxCount: 1);
|
||||
|
||||
private ulong lastEventSequence;
|
||||
@@ -257,6 +264,13 @@ public sealed class MxAccessEventQueue
|
||||
/// enqueues. The timeout is therefore a ceiling on how long the caller may sleep, not a
|
||||
/// poll period: it bounds how late a state change reaches the caller if some future path
|
||||
/// mutates the queue without signalling.
|
||||
/// <para>
|
||||
/// Precondition: <b>at most one waiter</b>. One permit is sufficient only because the
|
||||
/// queue has exactly one consumer (the worker's single event drain loop). A second
|
||||
/// concurrent waiter would find no permit left for it and degrade to the fallback
|
||||
/// timeout, so a design that needs multiple drainers must widen the signal rather than
|
||||
/// reuse this one.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="timeout">Maximum time to wait before the wait completes unsignalled.</param>
|
||||
/// <param name="cancellationToken">Token cancelling the wait.</param>
|
||||
|
||||
Reference in New Issue
Block a user