[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 в этом сценарии, будут оценены.