StephenCleary / AsyncEx

A helper library for async/await.
MIT License
3.53k stars 356 forks source link

Cross thread Sync context stealing #276

Open TomKuhn opened 1 year ago

TomKuhn commented 1 year ago

I'm getting some crazy UI thread deadlocks after using AsyncEx.Context. WinForms app.

Here's the UI thread, doing UI things: Application.Run(form); Here's the sync context on this thread: System.Windows.Forms.WindowsFormsSynchronizationContext, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 as expected.

However check this crazy callstack out:

System.dll!System.Collections.Concurrent.BlockingCollection.CheckDisposed() Line 1808 C# System.dll!System.Collections.Concurrent.BlockingCollection<System.Tuple<System.Threading.Tasks.Task, bool>>.TryAddWithNoTimeValidation(System.Tuple<System.Threading.Tasks.Task, bool> item, int millisecondsTimeout, System.Threading.CancellationToken cancellationToken) Line 414 C# Nito.AsyncEx.Context.dll!Nito.AsyncEx.AsyncContext.TaskQueue.TryAdd(System.Threading.Tasks.Task item, bool propagateExceptions) Line 57 C# Nito.AsyncEx.Context.dll!Nito.AsyncEx.AsyncContext.Enqueue(System.Threading.Tasks.Task task, bool propagateExceptions) Line 91 C# mscorlib.dll!System.Threading.Tasks.Task.ScheduleAndStart(bool needsProtection) Line 1946 C# mscorlib.dll!System.Threading.Tasks.Task.InternalStartNew(System.Threading.Tasks.Task creatingTask, System.Delegate action, object state, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.Tasks.TaskCreationOptions options, System.Threading.Tasks.InternalTaskOptions internalOptions, ref System.Threading.StackCrawlMark stackMark) Line 1294 C# mscorlib.dll!System.Threading.Tasks.TaskFactory.StartNew(System.Action action, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskCreationOptions creationOptions, System.Threading.Tasks.TaskScheduler scheduler) Line 401 C# Nito.AsyncEx.Tasks.dll!Nito.AsyncEx.TaskFactoryExtensions.Run(System.Threading.Tasks.TaskFactory this, System.Action action) Unknown Nito.AsyncEx.Context.dll!Nito.AsyncEx.AsyncContext.AsyncContextSynchronizationContext.Send(System.Threading.SendOrPostCallback d, object state) Line 62 C# System.dll!Microsoft.Win32.SystemEvents.SystemEventInvokeInfo.Invoke(bool checkFinalization, object[] args) Line 1634 C# System.dll!Microsoft.Win32.SystemEvents.RaiseEvent(bool checkFinalization, object key, object[] args) Line 1314 C# System.dll!Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(int msg, System.IntPtr wParam, System.IntPtr lParam) Line 1006 C# System.dll!Microsoft.Win32.SystemEvents.WindowProc(System.IntPtr hWnd, int msg, System.IntPtr wParam, System.IntPtr lParam) Line 1491 C#

The UI thread has somehow issued a post/send to an AsyncEx context which I use on other worker threads, but NEVER the UI thread! That Sync Context getting posted to has long been disposed, because this callstack ends in an exception thrown by the _queue in TaskQueue.cs:

System.ObjectDisposedException HResult=0x80131622 Message=The collection has been disposed. Object name: 'BlockingCollection'. Source=System StackTrace: at System.Collections.Concurrent.BlockingCollection`1.CheckDisposed() in f:\dd\NDP\fx\src\sys\system\collections\concurrent\BlockingCollection.cs:line 1810

It's very similar to this issue: https://ikriv.com/dev/dotnet/MysteriousHang#WhatToDo Which is also related to OnUserPreferenceChanged.

What I think is happening is that somehow a Control handle is being accidentally created on a non-UI thread, in which the Sync Context is AsyncEx's context. The control is auto-registering itself with SystemEvents to be notified when OnUserPreferenceChanged is raised. This registration also involves the current Sync Context, so it can post to it later.

The question is: How can I detect an erroneous handle creation on the non UI thread!

TomKuhn commented 1 year ago

This isn't some old version of framework, it's v4.7.2

TomKuhn commented 1 year ago

I think I've figured it out, was me being an idiot. I was on the UI thread doing this unintentionally

AsyncContext.Run(() => { // Create UI control here! });

This meant that when some UI controls registered for OnUserPreferenceChanged callbacks, they passed the AsyncContext to it, saying "call me back on this context" - which was long since disposed. I've fixed my code so that I don't use AsyncContext.Run if I'm on the UI thread (doh)