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 called from a task on a non-inlining TaskScheduler runs Invoke/InvokeAsync delegates off the owner thread, or deadlocks: task.RunSynchronously() uses TaskScheduler.Current #65
DoInvokes drains the queue with task.RunSynchronously(); (Invoker/Invoker.cs:204). With no argument, RunSynchronously targets TaskScheduler.Current. When DoInvokes is itself called from inside a task running on a custom scheduler whose TryExecuteTaskInline returns false, the task is queued back to that scheduler, and DoInvokes blocks until it runs somewhere else.
Repro
Scheduler hands work to the thread pool and refuses to inline. The owner loop runs as a task on this scheduler. Another thread calls inv.Invoke(...), then the owner calls inv.DoInvokes():
owner thread=4, Invoke'd action ran on thread=7, sameThread=False
RunSynchronously(TaskScheduler.Default) ran on 4, sameThread=True
Single dedicated-thread scheduler that refuses to inline. The owner is that scheduler's only thread:
DoInvokes still blocked after 3s (deadlock)
Why it matters
The library exists to guarantee that queued delegates run on the owner thread. In the first case that guarantee silently breaks, and code that touches thread-affine state runs on a pool thread. In the second case the owner thread hangs forever. TaskScheduler.FromCurrentSynchronizationContext() does inline on its own thread, so typical WPF/WinForms apps are not affected. Custom "dedicated thread" or limited-concurrency schedulers used for game loops and actors are.
Suggested fix
Use task.RunSynchronously(TaskScheduler.Default). The default scheduler always allows inlining here, and the repro confirms it then runs on the owner thread. An alternative is to stop using Task for the queue and complete a TaskCompletionSource after invoking the delegate directly.
Acceptance criteria
A test that calls DoInvokes from inside a task on a scheduler that refuses to inline asserts the delegate ran on the owner thread and that DoInvokes returns.
Priority: High. It breaks the library's core guarantee that queued delegates run on the owner thread, and in the single-thread-scheduler case it deadlocks the owner loop. It needs a custom non-inlining TaskScheduler, so WPF and WinForms apps are not affected. Game loops and actor schedulers, which are this library's audience, are.
Area / suggested assignee:Invoker/Invoker.cs:204, the task.RunSynchronously() call in DoInvokes. Owner: @matt-edmondson
Next step: Change the call to task.RunSynchronously(TaskScheduler.Default), a one-line fix, and add the non-inlining-scheduler test asserting that the delegate runs on the owner thread and that DoInvokes returns.
Activity
matt-edmondson commentedon Sep 29, 2026
Triage
TaskScheduler, so WPF and WinForms apps are not affected. Game loops and actor schedulers, which are this library's audience, are.Invoker/Invoker.cs:204, thetask.RunSynchronously()call inDoInvokes. Owner: @matt-edmondsonDoInvokesdrain as A BeginInvokeFailed handler that throws on one failure silently loses the rest: 3 failed TryBeginInvoke actions, 1 reported #66, DoInvokes() never returns while a producer keeps calling TryBeginInvoke, freezing the owner loop and starving Invoke/InvokeAsync callers #57 and DoInvokes runs TryBeginInvoke actions before earlier InvokeAsync work from the same thread, so submission order isn't preserved #60. No open PR covers it.task.RunSynchronously(TaskScheduler.Default), a one-line fix, and add the non-inlining-scheduler test asserting that the delegate runs on the owner thread and thatDoInvokesreturns.Generated by Claude Code