Ответ 1
Это будет немного длиннее. Прежде всего, спасибо Мэтту Смиту и Hans Passant за ваши идеи, они были очень полезны.
Проблема была вызвана добрым старым другом Application.DoEvents
, хотя и в новинке. Ганс отличный пост о том, почему DoEvents
- зло. К сожалению, я не могу избежать использования DoEvents
в этом элементе управления из-за синхронных ограничений API, создаваемых устаревшим неуправляемым хост-приложением (подробнее об этом в конце). Я хорошо знаю о существующих последствиях DoEvents
, но здесь я считаю, что у нас есть новый:
В потоке без явного конвейера WinForms (т.е. любой поток, который не ввел Application.Run
или Form.ShowDialog
), вызов Application.DoEvents
заменит текущий контекст синхронизации на значение по умолчанию SynchronizationContext
, если WindowsFormsSynchronizationContext.AutoInstall
is true
(по умолчанию это так).
Если это не ошибка, то это очень неприятное недокументированное поведение, которое может серьезно повлиять на разработчиков некоторых компонентов.
Вот простое консольное STA-приложение, воспроизводящее проблему. Обратите внимание, что WindowsFormsSynchronizationContext
получает (неправильно) заменяется на SynchronizationContext
в первом проходе Test
и не переходит во второй проход.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ConsoleApplication
{
class Program
{
[STAThreadAttribute]
static void Main(string[] args)
{
Debug.Print("ApartmentState: {0}", Thread.CurrentThread.ApartmentState.ToString());
Debug.Print("*** Test 1 ***");
Test();
SynchronizationContext.SetSynchronizationContext(null);
WindowsFormsSynchronizationContext.AutoInstall = false;
Debug.Print("*** Test 2 ***");
Test();
}
static void DumpSyncContext(string id, string message, object ctx)
{
Debug.Print("{0}: {1} ({2})", id, ctx != null ? ctx.GetType().Name : "null", message);
}
static void Test()
{
Debug.Print("WindowsFormsSynchronizationContext.AutoInstall: {0}", WindowsFormsSynchronizationContext.AutoInstall);
var ctx1 = SynchronizationContext.Current;
DumpSyncContext("ctx1", "before setting up the context", ctx1);
if (!(ctx1 is WindowsFormsSynchronizationContext))
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
var ctx2 = SynchronizationContext.Current;
DumpSyncContext("ctx2", "before Application.DoEvents", ctx2);
Application.DoEvents();
var ctx3 = SynchronizationContext.Current;
DumpSyncContext("ctx3", "after Application.DoEvents", ctx3);
Debug.Print("ctx3 == ctx1: {0}, ctx3 == ctx2: {1}", ctx3 == ctx1, ctx3 == ctx2);
}
}
}
Отладочный вывод:
ApartmentState: STA *** Test 1 *** WindowsFormsSynchronizationContext.AutoInstall: True ctx1: null (before setting up the context) ctx2: WindowsFormsSynchronizationContext (before Application.DoEvents) ctx3: SynchronizationContext (after Application.DoEvents) ctx3 == ctx1: False, ctx3 == ctx2: False *** Test 2 *** WindowsFormsSynchronizationContext.AutoInstall: False ctx1: null (before setting up the context) ctx2: WindowsFormsSynchronizationContext (before Application.DoEvents) ctx3: WindowsFormsSynchronizationContext (after Application.DoEvents) ctx3 == ctx1: False, ctx3 == ctx2: True
Было выполнено некоторое исследование реализации Framework Application.ThreadContext.RunMessageLoopInner
и WindowsFormsSynchronizationContext.InstalIifNeeded
/Uninstall
, чтобы понять, почему именно это происходит. Условие состоит в том, что поток в настоящее время не выполняет цикл сообщений Application
, как упоминалось выше. Соответствующий фрагмент из RunMessageLoopInner
:
if (this.messageLoopCount == 1)
{
WindowsFormsSynchronizationContext.InstallIfNeeded();
}
Тогда код внутри WindowsFormsSynchronizationContext.InstallIfNeeded
/Uninstall
пары методов не сохраняет/восстанавливает существующий контекст синхронизации. На данный момент я не уверен, что это ошибка или конструктивная особенность.
Решение состоит в том, чтобы отключить WindowsFormsSynchronizationContext.AutoInstall
, как это просто:
struct SyncContextSetup
{
public SyncContextSetup(bool autoInstall)
{
WindowsFormsSynchronizationContext.AutoInstall = autoInstall;
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
}
}
static readonly SyncContextSetup _syncContextSetup =
new SyncContextSetup(autoInstall: false);
Несколько слов о том, почему я использую Application.DoEvents
в первую очередь здесь. Это типичный асинхронный синхронный мостовой код, работающий в потоке пользовательского интерфейса, с использованием вложенного цикла сообщений. Это плохая практика, но устаревшее хост-приложение ожидает, что все интерфейсы API будут выполняться синхронно. Оригинальная проблема описана здесь . В какой-то более поздний момент я заменил CoWaitForMultipleHandles
комбинацией Application.DoEvents
/MsgWaitForMultipleObjects
, которая теперь выглядит так:
[EDITED] Самая последняя версия WaitWithDoEvents
здесь . [/отредактированы]
Идея заключалась в том, чтобы отправлять сообщения с использованием стандартного механизма .NET, а не полагаться на CoWaitForMultipleHandles
для этого. Это, когда я неявно ввел проблему с контекстом синхронизации из-за описанного поведения DoEvents
.
Унаследованное приложение в настоящее время переписывается с использованием современных технологий, а также контроль. Текущая реализация предназначена для существующих клиентов с Windows XP, которые не могут обновляться по причинам, не зависящим от нашего контроля.
Наконец, вот реализация пользовательского awaiter, о котором я упомянул в моем вопросе, как о возможности смягчения проблемы. Это был интересный опыт, и он работает, но он не может считаться правильным решением.
/// <summary>
/// AwaitHelpers - custom awaiters
/// WithContext continues on the control thread after await
/// E.g.: await TaskEx.Delay(1000).WithContext(this)
/// </summary>
public static class AwaitHelpers
{
public static ContextAwaiter<T> WithContext<T>(this Task<T> task, Control control, bool alwaysAsync = false)
{
return new ContextAwaiter<T>(task, control, alwaysAsync);
}
// ContextAwaiter<T>
public class ContextAwaiter<T> : INotifyCompletion
{
readonly Control _control;
readonly TaskAwaiter<T> _awaiter;
readonly bool _alwaysAsync;
public ContextAwaiter(Task<T> task, Control control, bool alwaysAsync)
{
_awaiter = task.GetAwaiter();
_control = control;
_alwaysAsync = alwaysAsync;
}
public ContextAwaiter<T> GetAwaiter() { return this; }
public bool IsCompleted { get { return !_alwaysAsync && _awaiter.IsCompleted; } }
public void OnCompleted(Action continuation)
{
if (_alwaysAsync || _control.InvokeRequired)
{
Action<Action> callback = (c) => _awaiter.OnCompleted(c);
_control.BeginInvoke(callback, continuation);
}
else
_awaiter.OnCompleted(continuation);
}
public T GetResult()
{
return _awaiter.GetResult();
}
}
}