View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0002261 | NoesisGUI | C# SDK | public | 2022-02-08 00:15 | 2022-06-06 12:39 |
Reporter | DavidYawCSpeed | Assigned To | sfernandez | ||
Priority | normal | Severity | major | Reproducibility | random |
Status | resolved | Resolution | fixed | ||
Product Version | 3.1.2 | ||||
Target Version | 3.1.5 | Fixed in Version | 3.1.5 | ||
Summary | 0002261: Dispatcher thread check should be done in managed land | ||||
Description | I 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. Constructor: 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) UpdateThingA(a); UpdateThingB(b); } 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 (https://github.com/Noesis/Managed/blob/master/Src/NoesisApp/Core/Src/ThreadSwitcher/DispatcherThreadSwitcher.cs), it looks at the Dispatcher's CheckAccess method to determine if it needs to switch threads. The Dispatcher.CheckAccess method (https://github.com/Noesis/Managed/blob/master/Src/Noesis/Core/Src/Core/Dispatcher.cs) 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 Reproduce | Reproduction 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. | ||||
Tags | No tags attached. | ||||
Platform | Any | ||||
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. |
|
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. |
|
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 |