これは少し長くなります。まず、Matt SmithとHans Passantのアイデアに感謝します。彼らは非常に役に立ちました。
Application.DoEvents
この問題は、斬新な方法ではありますが、古き良き友人によって引き起こされました。ハンスはなぜ悪であるかについて素晴らしい記事を書いています。DoEvents
残念ながら、DoEvents
このコントロールでの使用を避けることはできません。これは、レガシ アンマネージド ホスト アプリによる同期 API の制限のためです (詳細は最後に説明します)。の既存の意味を十分に認識してDoEvents
いますが、ここでは新しい意味があると思います。
明示的な WinForms メッセージ ループのないスレッド (つまり、Application.Run
またはに入っていないスレッドForm.ShowDialog
) では、 を呼び出すApplication.DoEvents
と、現在の同期コンテキストが既定の に置き換えSynchronizationContext
られます (既定でWindowsFormsSynchronizationContext.AutoInstall
はtrue
そうなります)。
バグでない場合は、文書化されていない非常に不快な動作であり、一部のコンポーネント開発者に深刻な影響を与える可能性があります。
問題を再現する簡単なコンソール STA アプリを次に示します。の最初のパスで(誤って) が に置き換えられ、2 番目のパスでは置き換えられないことに注意してください。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);
}
}
}
デバッグ出力:
アパート州: STA
*** テスト 1 ***
WindowsFormsSynchronizationContext.AutoInstall: True
ctx1: null (コンテキストを設定する前)
ctx2: WindowsFormsSynchronizationContext (Application.DoEvents の前)
ctx3: SynchronizationContext (Application.DoEvents の後)
ctx3 == ctx1: 偽、ctx3 == ctx2: 偽
*** テスト 2 ***
WindowsFormsSynchronizationContext.AutoInstall: False
ctx1: null (コンテキストを設定する前)
ctx2: WindowsFormsSynchronizationContext (Application.DoEvents の前)
ctx3: WindowsFormsSynchronizationContext (Application.DoEvents の後)
ctx3 == ctx1: 偽、ctx3 == ctx2: 真
Application.ThreadContext.RunMessageLoopInner
フレームワークのand 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
なぜ私が最初にここで使用するのかについてのいくつかの言葉. これは、入れ子になったメッセージ ループを使用して、UI スレッドで実行される典型的な非同期から同期へのブリッジ コードです。これは悪い習慣ですが、レガシ ホスト アプリはすべての API が同期的に完了することを想定しています。元の問題はここで説明されています。後で/CoWaitForMultipleHandles
の組み合わせに置き換えたところ、次のようになりました。Application.DoEvents
MsgWaitForMultipleObjects
[編集済み]の最新バージョンWaitWithDoEvents
はこちらです。[/編集済み]
アイデアは、.NET 標準メカニズムに依存するのではなく、それを使用してメッセージをディスパッチするCoWaitForMultipleHandles
ことでした。の動作が説明されているため、同期コンテキストの問題を暗黙のうちに導入したのはそのときですDoEvents
。
レガシ アプリは現在、最新のテクノロジを使用して書き直されており、コントロールも同様です。現在の実装は、Windows XP を使用している既存のお客様で、当社の管理の及ばない理由でアップグレードできない方を対象としています。
最後に、問題を軽減するためのオプションとして質問で言及したカスタム awaiter の実装を次に示します。それは興味深い経験であり、機能しますが、適切な解決策とは見なされません。
/// <summary>
/// AwaitHelpers - custom awaiters
/// WithContext continues on the control's 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();
}
}
}