View Issue Details

IDProjectCategoryView StatusLast Update
0002261NoesisGUIC# SDKpublic2022-06-06 12:39
ReporterDavidYawCSpeed Assigned Tosfernandez  
Status resolvedResolutionfixed 
Product Version3.1.2 
Target Version3.1.5Fixed in Version3.1.5 
Summary0002261: Dispatcher thread check should be done in managed land
DescriptionI got an error, which had a stack trace like this:
at MyClass.UpdateThingA()
at MyClass.UpdateStuffInUI()
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_1(Object state)
at System.Threading.QueueUserWorkItemCallback.<>c.<.cctor>b__6_0(QueueUserWorkItemCallback quwi)
at System.Threading.ExecutionContext.RunForThreadPoolUnsafe[TState](ExecutionContext executionContext, Action`1 callback, TState& state)
at System.Threading.QueueUserWorkItemCallback.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()

The problem is, that stack trace should be impossible to get.

System.Threading.Timer timer = new System.Threading.Timer(_ => UpdateStuffInUI());

private async void UpdateStuffInUI()
  // Compute some stuff while on the background thread that the Timer uses.
  string a = timeConsumingMethod();
  string b = timeConsumingMethod();

  await ThreadSwitcher.ResumeForegroundAsync(Application.Current.Dispatcher)


That stack trace should be impossible. I should see the dispatcher in the stack, like so:
at UIShell.App.DemoAsyncMethod()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread)
at Noesis.Dispatcher.DispatcherOperation.Invoke(Delegate callback, Object args, SynchronizationContext context)
at Noesis.Dispatcher.ProcessQueue()
at Noesis.View.Update(Double timeInSeconds)
at NoesisApp.Window.Render(Double time)
at NoesisApp.Application.<Run>b__55_0(Display d)
at NoesisApp.Win32Display.EnterMessageLoop(Boolean runInBackground)
at NoesisApp.Application.Run()
at UIShell.App.Main(String[] args)

Looking at DispatcherThreadSwitcher (, it looks at the Dispatcher's CheckAccess method to determine if it needs to switch threads. The Dispatcher.CheckAccess method ( looks at the current thread's Thread ID vs. the Dispatcher's Thread ID. Both thread IDs are retrieved from unmanaged method Noesis_GetCurrentThreadId(), which I obviously don't have access to, but looks like it returns the unmanaged thread ID (Win32 method GetThreadId on Windows, some Posix function on Linux). In a quick test on my Windows system, the debugger showed that Dispatcher.ThreadID was 32756, which matched the unmanaged thread ID shown in the debugger. The Managed Thread ID was 1.

In my reading, it looks like the relationship between managed and unmanaged threads is not 1:1. It looks like a managed thread might be run on different unmanaged threads. That would explain why the thread ID that the dispatcher knows and the ThreadPoolWorkQueue thread that my method ran on ended up having the same thread ID.

I cannot think of any reason why the call to ResumeForegroundAsync wouldn't switch threads, other than this changing relationship between unmanaged and managed.

I suggest that CheckAccess, VerifyAccess, and CurrentDispatcher all switch to using managed thread ID, not unmanaged. In the Dispatcher class, this should be a matter of just replacing calls to the Dispatcher's helper property CurrentThreadID with a call to Thread.CurrentThread.ManagedThreadId. I didn't trace through any other objects that have a Thread ID that would need to change.
Steps To ReproduceReproduction of this issue appears to be random. It requires that the ThreadPool thread be on the unmanaged thread that used to host the Dispatcher thread, which is unlikely.
TagsNo tags attached.




2022-02-16 21:02

manager   ~0007810

We need to check the unmanged thread id because DispatcherObjects (UI elements) only know about the unmanaged thread when created, so we can only associate a Dispatcher with an unmanaged thread id.
But we can add an extra check for the managed thread id in C# Dispatcher's CheckAccess to enqueue jobs when calling from different managed threads. This will fix the problems you are having.


2022-02-17 03:42

reporter   ~0007817

OK, that sounds reasonable.

I can see that the Dispatcher could know the managed ID, since it gets passed to Dispatcher's constructor from Dispatcher.FromThreadId(). If that value was passed to the rest of the objects, that would be enough, but they're probably retrieving the unmanaged Thread ID directly in C++.

One possible additional change that might be useful: Call Thread.BeginThreadAffinity() at some point during application startup, and Thread.EndThreadAffinity() at some point during application shutdown. (At the beginning & end of Application.Run(), maybe?) This will keep the dispatcher managed thread running on the same unmanaged thread. The documentation isn't clear on exactly when the managed thread & unmanaged thread can switch, but BeginThreadAffinity looks like it will lock that down.

Issue History

Date Modified Username Field Change
2022-02-08 00:15 DavidYawCSpeed New Issue
2022-02-08 00:16 DavidYawCSpeed Description Updated
2022-02-08 00:19 DavidYawCSpeed Description Updated
2022-02-08 11:36 sfernandez Assigned To => sfernandez
2022-02-08 11:36 sfernandez Status new => assigned
2022-02-08 11:36 sfernandez Target Version => 3.1.3
2022-02-09 21:12 DavidYawCSpeed Description Updated
2022-02-14 17:52 sfernandez Target Version 3.1.3 => 3.1.4
2022-02-16 21:02 sfernandez Status assigned => feedback
2022-02-16 21:02 sfernandez Note Added: 0007810
2022-02-17 03:42 DavidYawCSpeed Note Added: 0007817
2022-02-17 03:42 DavidYawCSpeed Status feedback => assigned
2022-03-17 21:31 sfernandez Target Version 3.1.4 => 3.1.5
2022-06-06 12:39 sfernandez Status assigned => resolved
2022-06-06 12:39 sfernandez Resolution open => fixed
2022-06-06 12:39 sfernandez Fixed in Version => 3.1.5