System.Windows.Forms.WindowsFormsSynchronizationContextは、SynchronizationContextクラスの派生クラスです。
Windows Formsにて、同期コンテキストを処理する際に裏で利用されています。
どんな役割を担っているのかをざっくりと言うと
別スレッドで動作している処理から、特定のコンテキスト(スレッド)上で処理が動くようにしてくれる機能
と思っていると分かりやすいかと思います。
この概念、UIを持つアプリの場合はすごく重要になります。
なぜなら、Windows FormsもWPFもJava Swingも、以下の決まり事があるからです。
UIを更新できるのは、UIスレッドからしか出来ない。
このあたりの事情に関しては、Java(一つは.NET)の記事になりますが
昔書いたので、よろしければご参照下さい。
- Swingスレッド処理-001
- Swingスレッド処理-002(イベントディスパッチスレッドについて)
- Swingスレッド処理-003(時間のかかる処理の実行について)
- Windows Formsスレッド処理-001 (Windows Formsでのスレッド処理について)
全ての処理をUIスレッド上で行えば問題ありませんが、その代わり時間がかかる処理の場合は
UIがフリーズする事になります。なので、どうしてもマルチスレッドで処理する必要がある
部分が出てきます。そのようなときにSynchronizationContextが裏で頑張ってくれています。
SynchronizationContextやAsyncOperationManagerなどの同期コンテキスト周りに関しては
- .NET クラスライブラリ探訪-018 (AsyncOperation, AsyncOperationManager,SynchronizationContext)(コンテキスト,コンテキストの同期,非同期処理)
にて、記述していますので、ご参考までにどうぞ。
SynchronizationContextは、各機能毎に派生クラスが用意されており
Windows Formsの場合は前述したWindowsFormsSynchronizationContextクラスとなり
WPFやSilverlightの場合はDispatcherSynchronizationContextとなります。
これらのクラスは、非同期処理を行う際に裏で頑張ってくれているので
普段は見ることがありませんが、.NETアプリを作成する上で常に裏側で
存在していますので、知っておくと便利です。
WindowsFormsSynchronizationContextは、通常最初のフォームが作成された
タイミングで、自動的に作成されインストールされます。
あまり無いとは思いますが、以下のようにするとこの挙動を変更することも出来ます。
WindowsFormsSynchronizationContext.AutoInstall = false;
上記のようにすると、自動的にインストールされなくなります。
その場合、初期時は既定のSynchronizationContextとなります。
WindowsFormsSynchronizationContextクラスの基底クラスである
SynchronizationContextクラスには、以下のメソッドがあります。
- Send
- Post
Sendメソッドは、指定されたデリゲートを紐づくコンテキストに対して同期にキュー登録します。
Postメソッドは、指定されたデリゲートを紐づくコンテキストに対して非同期でキュー登録します。
WindowsFormsSynchronizationContextの場合、コンテキストが対応するのは
UIスレッドとなりますので、上記のメソッドはそのまま
- SendMessage
- PostMessage
と同じような動きになります。
以下、サンプルです。
サンプルでは、各イベントハンドラにてSendとPostを発行して
どのタイミングで出力が行われるかを見ています。
#region WindowsFormsSynchronizationContextSamples-01 /// <summary> /// WindowsFormsSynchronizationContextクラスについてのサンプルです。 /// </summary> /// <remarks> /// WindowsFormsSynchronizationContextは、SynchronizationContextクラスの派生クラスです。 /// デフォルトでは、Windows Formsにて、最初のフォームが作成された際に自動的に設定されます。 /// (AutoInstall静的プロパティにて、動作を変更可能。) /// </remakrs> public class WindowsFormsSynchronizationContextSamples01 : IExecutable { class SampleForm : Form { public string ContextTypeName { get; set; } public SampleForm() { Load += (s, e) => { // // UIスレッドのスレッドIDを表示. // PrintMessageAndThreadId("UI Thread"); // // 現在の同期コンテキストを取得. // Windows Formsの場合は、WinFormsSynchronizationContextとなる。 // SynchronizationContext context = SynchronizationContext.Current; ContextTypeName = context.ToString(); // // Sendは、同期コンテキストに対して同期メッセージを送る。 // Postは、同期コンテキストに対して非同期メッセージを送る。 // // つまり、SendMessageとPostMessageと同じ. // context.Send((obj) => { PrintMessageAndThreadId("Send"); }, null); context.Post((obj) => { PrintMessageAndThreadId("Post"); }, null); // // UIスレッドと関係ない別のスレッド. // Task.Factory.StartNew(() => { PrintMessageAndThreadId("Task.Factory"); }); PrintMessageAndThreadId("Form.Load"); Close(); }; FormClosing += (s, e) => { // // SendとPostを呼び出し、どのタイミングで出力されるか確認. // SynchronizationContext context = SynchronizationContext.Current; context.Send((obj) => { PrintMessageAndThreadId("Send--2"); }, null); context.Post((obj) => { PrintMessageAndThreadId("Post--2"); }, null); // // UIスレッドと関係ない別のスレッド. // Task.Factory.StartNew(() => { PrintMessageAndThreadId("Task.Factory"); }); PrintMessageAndThreadId("Form.FormClosing"); }; FormClosed += (s, e) => { // // SendとPostを呼び出し、どのタイミングで出力されるか確認. // SynchronizationContext context = SynchronizationContext.Current; context.Send((obj) => { PrintMessageAndThreadId("Send--3"); }, null); context.Post((obj) => { PrintMessageAndThreadId("Post--3"); }, null); // // UIスレッドと関係ない別のスレッド. // Task.Factory.StartNew(() => { PrintMessageAndThreadId("Task.Factory"); }); PrintMessageAndThreadId("Form.FormClosed"); }; } private void PrintMessageAndThreadId(string message) { Console.WriteLine("{0,-17}, スレッドID: {1}", message, Thread.CurrentThread.ManagedThreadId); } } [STAThread] public void Execute() { // // SynchronizationContextは、同期コンテキストを様々な同期モデルに反映させるための // 処理を提供するクラスである。 // // 派生クラスとして以下のクラスが存在する。 // ・WindowsFormsSynchronizationContext (WinForms用) // ・DispatcherSynchronizationContext (WPF用) // // 基本的に、WinFormsもしくはWPFを利用している状態で // UIスレッドとは別のスレッドから、UIを更新する際に裏で利用されているクラスである。 // (BackgroundWorkerも、このクラスを利用してUIスレッドに更新をかけている。) // // 現在のスレッドのSynchronizationContextを取得するには、Current静的プロパティを利用する。 // 特定のSynchronizationContextを強制的に設定するには、SetSynchronizationContextメソッドを利用する。 // // デフォルトでは、独自に作成したスレッドの場合 // SynchronizationContext.Currentの戻り値はnullとなる。 // Console.WriteLine( "現在のスレッドでのSynchronizationContextの状態:{0}", SynchronizationContext.Current == null ? "NULL" : SynchronizationContext.Current.ToString() ); // // フォームを起動し、値を確認. // Application.EnableVisualStyles(); SampleForm aForm = new SampleForm(); Application.Run(aForm); Console.WriteLine("WinFormsでのSynchronizationContextの型名:{0}", aForm.ContextTypeName); } } #endregion
実行結果は以下のようになります。
現在のスレッドでのSynchronizationContextの状態:NULL UI Thread , スレッドID: 1 Send , スレッドID: 1 Form.Load , スレッドID: 1 Post , スレッドID: 1 Send--2 , スレッドID: 1 Form.FormClosing , スレッドID: 1 Post--2 , スレッドID: 1 Send--3 , スレッドID: 1 Form.FormClosed , スレッドID: 1 Task.Factory , スレッドID: 4 Task.Factory , スレッドID: 4 Task.Factory , スレッドID: 4 Post--3 , スレッドID: 1 WinFormsでのSynchronizationContextの型名:System.Windows.Forms.WindowsFormsSynchronizationContext
これを見ると、Sendはすぐに実行されており(同期)、Postは逆にイベントが終わった後に実行されているのが分かります。(非同期)
ちなみに、コンソールアプリの場合は、既定のSynchronizationContextしか所持していません。
あとWindowsサービスアプリの場合同様です。
ASP.NETの場合は、AspNetSynchronizationContextというクラスが同期コンテキストを処理しています。
また、タスク並列ライブラリ(TPL)には、同期コンテキストを利用するタスクを作成する機能が
元々備わっています。なので、今後のバージョンではSynchronizationContextをわざわざ利用することは
少なくなるのでしょうね。
// 現在のSynchronizationContextを利用するTaskSchedulerを取得 TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext(); // タスク作成 Task t = Task.Factory.StartNew(() => { // // この中の処理は、UIスレッドで処理される。 // }, CancellationToken.None, TaskCreationOptions.None, scheduler );
【余談】
BackgroundWorkerを利用されたことがある人は多いと思いますが
何故、DoWorkハンドラは別スレッドで動いて、RunWorkerCompletedハンドラは
UIスレッドで動くのか?って疑問を感じたことないでしょうか。
それは、BackgroundWorkerが内部でSynchronizationContextを利用して
UIスレッド上で処理が動作するようにしているからです。
(実際にはAsyncOperation周りを利用していると思われますが。)
これが分かると、何故BackgroundWorkerを入れ子にして利用すると
UIを更新する際におかしくなるかが分かります。
以下のようなコードがあったとします。
// 現在UIスレッド上とする. var worker = new BackgroundWorker(); worker.DoWork += (s, e) => { // // この中は、別スレッドで処理される // // 2つ目のBackgroundWorkerを作成. // このとき、2つ目のBackgroundWorkerは作成時に // 現在のスレッドに紐付いているSynchronizationContextをキャプチャして保持する. // その際、現在のスレッドはUIスレッドではないので // 取得されるSynchronizationContextは、既定のSynchronizationContextとなる。 // なので、2つ目のBackgroundWorkerのRunWorkerCompletedにてUIを更新しようと // すると、最悪例外が発生する。 var worker2 = new BackgroundWorker(); worker2.DoWork += (...); worker2.RunWorkerCompleted += (ss, ee) => { // // この中の処理が、UIスレッドで走らない. // }; }; worker.RunWorkerCompleted += (s, e) => { // // この中は、UIスレッドで処理される。 // }; worker.RunWorkerAsync();
一つ目のBackgroundWorkerは、UIスレッド上で作成されているので
キャプチャしたSynchronizationContextは、WindowsFormsSynchronizationContextとなります。
なので、RunWorkerCompletedの時にちゃんとUIスレッドにて実行されます。
それに対して、2つ目のBackgroundWorkerは、別スレッド上で作成されているので
キャプチャしたSynchronizationContextは、既定のSynchronizationContextとなります。
なので、RunWorkerCompletedの時にちゃんとUIスレッドで実行されていません。
このような状態で、UIを更新すると最悪意味不明な例外が発生する可能性があります。
参考にしたリソースは以下です。
- SynchronizationContext こそすべて
- SynchronizationContext クラス
================================
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場