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
DoInvokes runs TryBeginInvoke actions before earlier InvokeAsync work from the same thread, so submission order isn't preserved #60
Invoker keeps two separate queues: BeginInvokeQueue for TryBeginInvoke, and TaskQueue for Invoke/InvokeAsync. DoInvokes (Invoker/Invoker.cs, ~lines 164–172) empties the whole BeginInvokeQueue first and only then empties TaskQueue:
So on a single pump, work sent through TryBeginInvoke always runs before work sent earlier through InvokeAsync, even when one thread made both calls in that order.
On the next DoInvokes(), UseTexture runs before CreateTexture, so it uses a resource that doesn't exist yet. A reproduction that makes both calls from one background thread and then pumps on the owner thread prints:
The TryBeginInvoke remarks only promise FIFO order among TryBeginInvoke actions. Nothing in the XML docs, README or CLAUDE.md warns that the two APIs don't keep order relative to each other. Callers reasonably expect a single marshalling queue to run work in submission order.
Suggested fix
Preferred: preserve submission order across both APIs. For example, tag each queued item with a shared Interlocked.Increment sequence number and have DoInvokes merge the two queues by sequence. Alternatively, route the Task path through the same queue as TryBeginInvoke.
If the split is deliberate, e.g. to keep TryBeginInvoke allocation-free, document on both TryBeginInvoke and DoInvokes (and in the README) that the two kinds of work are drained separately and aren't ordered relative to each other.
Acceptance: a test submitting InvokeAsync(A) and then TryBeginInvoke(B) from one thread observes A before B, or the documentation states that this isn't guaranteed.
Preserve submission order across both APIs. This is the preferred fix. InvokeAsync(A) followed by TryBeginInvoke(B) from one thread must run A before B, so documenting the split is not enough.
The mechanism is left to the implementer. Two options are:
a shared Interlocked.Increment sequence number, merged in DoInvokes
routing the Task path through the same queue
Next reader: implement ordered draining in DoInvokes, and add the acceptance test (InvokeAsync(A) then TryBeginInvoke(B) from one thread, observe A before B). Update the TryBeginInvoke remarks to state the cross-API ordering guarantee.
Priority: Medium. From a single thread, InvokeAsync(A) followed by TryBeginInvoke(B) runs B first. That silently reorders work on the owner thread, and can make a follow-up use a resource before it exists (the texture example). Code that mixes the two APIs is affected; code using only one isn't.
Area / suggested assignment:Invoker/Invoker.cs (DoInvokes, ~lines 164-172, plus the queue fields and the TryBeginInvoke remarks)
Notes: The maintainer decision is to preserve submission order across both APIs; documenting the split is not enough. A shared Interlocked sequence number keeps TryBeginInvoke allocation-light. A single queue is simpler and makes #56's exception handling easier to get right in one place.
Activity
matt-edmondson commentedon Sep 28, 2026
Decision (maintainer, 2026-09-28)
InvokeAsync(A)followed byTryBeginInvoke(B)from one thread must run A before B, so documenting the split is not enough.Interlocked.Incrementsequence number, merged inDoInvokesTaskpath through the same queueNext reader: implement ordered draining in
DoInvokes, and add the acceptance test (InvokeAsync(A)thenTryBeginInvoke(B)from one thread, observe A before B). Update theTryBeginInvokeremarks to state the cross-API ordering guarantee.Generated by Claude Code
matt-edmondson commentedon Sep 28, 2026
Triage
InvokeAsync(A)followed byTryBeginInvoke(B)runs B first. That silently reorders work on the owner thread, and can make a follow-up use a resource before it exists (the texture example). Code that mixes the two APIs is affected; code using only one isn't.Invoker/Invoker.cs(DoInvokes, ~lines 164-172, plus the queue fields and theTryBeginInvokeremarks)TryBeginInvokeaction abortsDoInvokes) and Code afterawait InvokeAsync(...)resumes on the owner thread inside DoInvokes, stalling the UI/render loop #53 (await InvokeAsynccontinuations resume insideDoInvokes). All three change the same drain loop, so coordinate them or land them together.Notes: The maintainer decision is to preserve submission order across both APIs; documenting the split is not enough. A shared
Interlockedsequence number keepsTryBeginInvokeallocation-light. A single queue is simpler and makes #56's exception handling easier to get right in one place.Generated by Claude Code