You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A nested DoInvokes() can't run the item the outer pump has already dequeued, so "pump until the worker finishes" hangs (regression from #80 / f81899e) #85
Since f81899e ("Run InvokeAsync and TryBeginInvoke work in submission order", #80), DoInvokes() takes the next item from each queue before running the current one (Invoker/Invoker.cs:289-314):
While action A runs, the head of the other queue (e.g. an Invoke task X) has already been dequeued into the outer pump's local nextTask. It's in neither queue. If A calls invoker.DoInvokes() itself (a DoEvents-style "pump until X is done" loop on the owner thread), the inner pump can't see X, so X can't run until A returns. A is waiting for X, so it never returns.
varinvoker=newInvoker();boolworkerDone=false;invoker.TryBeginInvoke(()=>{// A, queued firstvarsw=Stopwatch.StartNew();while(!Volatile.Read(refworkerDone)){invoker.DoInvokes();if(sw.ElapsedMilliseconds>3000){Console.WriteLine("A: TIMED OUT - X never ran");return;}Thread.Sleep(1);}Console.WriteLine("A: worker finished");});newThread(()=>{invoker.Invoke(()=>Console.WriteLine("X: ran on owner"));workerDone=true;}).Start();Thread.Sleep(200);// X is now queued behind Ainvoker.DoInvokes();// owner thread
At HEAD:
A: TIMED OUT - X never ran
X: ran on owner
Without the timeout, A spins forever and the worker stays blocked in Invoke forever.
With the pre-#80Invoker.cs/BoundedMpscQueue.cs (git show f81899e^:...), the same program prints X: ran on owner then A: worker finished.
The reverse case hangs the same way: an Invoke delegate that pumps while waiting on a later TryBeginInvoke action. Nested pumps also run later items before the one the outer pump is holding, which breaks the submission order #80 was meant to guarantee.
Why it matters
Pumping DoInvokes() from inside owner-thread work while waiting for a background result is a common pattern, for example a modal loop or a synchronous wait in UI code. It worked before #80 and now deadlocks whenever the awaited item has a higher sequence number than the running item and sits in the other queue.
Suggested fix
Don't dequeue ahead of time. Each iteration, peek at both heads, dequeue only the one with the lower number, and run it straight away:
ConcurrentQueue.TryPeek for the task queue
a TryPeek on BoundedMpscQueue that reads the cell when seq == pos + 1
Charge the per-queue budgets per item, as now.
Acceptance: a test where a TryBeginInvoke action pumps DoInvokes() until an earlier-queued-from-another-thread Invoke completes finishes promptly, and the mirror case (an Invoke delegate pumping for a TryBeginInvoke action) does too. Existing ordering tests from #60/#80 still pass.
Related, but a different problem: #83 (the budgets are read at different moments).
Priority: High. Pumping DoInvokes() from owner-thread work while waiting on a background Invoke worked before Run InvokeAsync and TryBeginInvoke work in submission order [patch] #80 and now deadlocks: the worker stays blocked in Invoke and the owner spins forever. Modal loops and synchronous waits in UI code use this pattern.
Area:Invoker/Invoker.csDoInvokes() (dequeue-ahead loop, ~L289-314) and BoundedMpscQueue (needs a TryPeek)
Activity
matt-edmondson commentedon Oct 29, 2026
Triage
DoInvokes()from owner-thread work while waiting on a backgroundInvokeworked before Run InvokeAsync and TryBeginInvoke work in submission order [patch] #80 and now deadlocks: the worker stays blocked inInvokeand the owner spins forever. Modal loops and synchronous waits in UI code use this pattern.Invoker/Invoker.csDoInvokes()(dequeue-ahead loop, ~L289-314) andBoundedMpscQueue(needs aTryPeek)Generated by Claude Code