[EDITED] Это, кажется, ошибка в реализации Framework Application.DoEvents, о котором я сообщил здесь. Восстановление неправильного контекста синхронизации в потоке пользовательского интерфейса может серьезно повлиять на разработчиков компонентов, таких как я. Целью награды является привлечение большего внимания к этой проблеме и вознаграждение @MattSmith, ответ которой помог отслеживать ее.
Я отвечаю за компонент .NET WinForms UserControl
, открытый как ActiveX для устаревшего неуправляемого приложения, через COM-взаимодействие. Требование времени выполнения -.NET 4.0 + Microsoft.Bcl.Async.
Компонент получает экземпляр и используется в главном потоке пользовательского интерфейса STA. В его реализации используется async/await
, поэтому он ожидает, что экземпляр сериализующего контекста синхронизации был установлен в текущем потоке (то есть, WindowsFormsSynchronizationContext
).
Обычно WindowsFormsSynchronizationContext
настраивается на Application.Run
, где выполняется цикл сообщений управляемого приложения. Естественно, это не относится к неуправляемому хост-приложению, и я не контролирую это. Конечно, хост-приложение по-прежнему имеет свой собственный классический цикл сообщений Windows, поэтому не следует сериализовать обратные вызовы await
продолжения.
Однако ни одно из решений, которые я придумал, не совершенен или даже работает правильно. Здесь искусственный пример, где метод Test
вызывается приложением-хостом:
Task testTask;
public void Test()
{
this.testTask = TestAsync();
}
async Task TestAsync()
{
Debug.Print("thread before await: {0}", Thread.CurrentThread.ManagedThreadId);
var ctx1 = SynchronizationContext.Current;
Debug.Print("ctx1: {0}", ctx1 != null? ctx1.GetType().Name: null);
if (!(ctx1 is WindowsFormsSynchronizationContext))
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
var ctx2 = SynchronizationContext.Current;
Debug.Print("ctx2: {0}", ctx2.GetType().Name);
await TaskEx.Delay(1000);
Debug.WriteLine("thread after await: {0}", Thread.CurrentThread.ManagedThreadId);
var ctx3 = SynchronizationContext.Current;
Debug.Print("ctx3: {0}", ctx3 != null? ctx3.GetType().Name: null);
Debug.Print("ctx3 == ctx1: {0}, ctx3 == ctx2: {1}", ctx3 == ctx1, ctx3 == ctx2);
}
Отладочный вывод:
thread before await: 1 ctx1: SynchronizationContext ctx2: WindowsFormsSynchronizationContext thread after await: 1 ctx3: SynchronizationContext ctx3 == ctx1: True, ctx3 == ctx2: False
Хотя он продолжается в том же потоке, WindowsFormsSynchronizationContext
контекст, который я устанавливаю в текущем потоке до await
, получает reset по умолчанию SynchronizationContext
после него, для некоторых причина.
Почему он получает reset? Я проверил, что мой компонент является единственным компонентом .NET, который используется этим приложением. Само приложение действительно правильно вызывает CoInitialize/OleInitialize
.
Я также пробовал настройку WindowsFormsSynchronizationContext
в конструкторе статического одноэлементного объекта, поэтому он устанавливается в потоке, когда моя управляемая сборка загружается. Это не помогло: когда Test
позже вызывается в том же потоке, контекст уже был reset по умолчанию.
Я рассматриваю возможность использования пользовательского awaiter для планирования await
обратных вызовов через control.BeginInvoke
моего элемента управления, поэтому приведенное выше будет выглядеть как await TaskEx.Delay().WithContext(control)
. Это должно работать для моего собственного awaits
, если хост-приложение продолжает накачивать сообщения, но не для awaits
внутри любой из сторонних сборок, на которые может ссылаться моя сборка.
Я все еще разбираюсь в этом. Любые идеи о том, как сохранить правильное сходство потоков для await
в этом сценарии, будут оценены.