by WebSurfer
2020年9月30日 15:53
SynchronizationContext とは何かを調べましたので、十分とは言えないまでも取りあえず分かった(分かった気になっただけかもしれませんが)ことを備忘録として書いておきます。
非同期プログラミングの勉強の際に、Microsoft のドキュメント「非同期プログラミングのベストプラクティス」を読んだのですが、詳しい説明なしでいきなり、
"・・・async void メソッドが開始されたときにアクティブだった SynchronizationContext で直接発生します・・・・・・未完了の Task を待機するときは、現在の "コンテキスト" がキャプチャされ、Task が完了するときのメソッドの再開に使用されます。この "コンテキスト" は現在の SynchronizationContext で・・・"
というように SynchronizationContext という言葉が出てきます。上記の他にも何か所かで出てくるのですが、非同期プログラミングを理解するのに重要なキーワードのようで、それが何かを理解してない自分には、そのドキュメントの内容が半分も理解できませんでした。
Microsoft のドキュメント「SynchronizationContext クラス」によると .NET Framework 2.0 の時代から存在していたようで、以下のように説明されています。
"同期コンテキストをさまざまな同期モデルに反映させるための基本機能を提供します。SynchronizationContext クラスは、同期なしのフリー スレッド コンテキストを提供する基本クラスです。このクラスで実装する同期モデルの目的は、共通言語ランタイムの非同期または同期の内部操作をさまざまな同期モデルで正しく動作させることです。"
残念ながらその説明では自分の頭では全く理解できません。なので、そのドキュメントからリンクが貼ってある MSDN マガジンの記事「並列コンピューティング - SynchronizationContext こそすべて」を読んでみました。
他に「async/await と SynchronizationContext」、「async/awaitと同時実行制御」、「ASP.NET の非同期でありがちな Deadlock を克服する」という記事を参考にさせていただきました。これらは Microsoft のドキュメントに比べれば少し分かりやすかったです。
上の記事を読んで、SynchronizationContext とは何かを理解する上で重要と思った点を以下にまとめておきます。無知ゆえの独断と偏見による個人的解釈も含まれているので注意してください。
-
マルチスレッドプログラムでは、あるスレッドから別のスレッドに作業単位を受け渡す必要が生じることがよくある。SynchronizationContext クラスはそれを支援するツール。
-
Windows Forms, WPF, ASP.NET, Silverlight, コンソールアプリなど、すべての .NET プログラムには SynchronizationContext の概念が含まれる。(公式ドキュメントを見ると .NET だけでなく Core なども適用対象に含まれているようです)
-
古くはメッセージキューを使用して作業単位を受け渡していたが、.NET Framework が登場した時に汎用ソリューションとして ISynchronizeInvoke が考案され、その後 .NET Framework 2.0 で ASP.NET の非同期プログラミングをサポートするため SynchronizationContext に置き換えられた。
-
SynchronizationContext の特徴は (1) 作業単位をコンテキストのキューにする (unit of work is queued to a context rather than a specific thread)、(2) 全てのスレッドは "現在の" コンテキストを持つ (every thread has a “current” contex)、(3) 未完了の非同期操作の数を管理する (it keeps a count of outstanding asynchronous operations)。
-
Windows Forms, WPF, ASP.NET に使用されている SynchronizationContext はそれぞれ実装が異なっており、順に以下の通りとなる。(Current プロパティで確認できる)
WindowsFormsSynchronizationContext
DispatcherSynchronizationContext
AspNetSynchronizationContext (.NET 4.5 以降)
-
Windows Forms, WPF などの GUI アプリでは、await で待機するとき現在の SynchronizationContext がキャプチャされ、await が完了するとキャプチャした SynchronizationContext で続きを実行する。その際に使われるスレッドは await 前後で同じ、即ち UI スレッドになる。
-
ASP.NET でも await 前後での SynchronizationContext のキャプチャと続きの実行は GUI アプリと同様だが、await 前後で HttpContext が同じになるようにしている。await 前までに使っていたスレッドはスレッドプールに戻し、await が完了後の処理はスレッドプールから新たにスレッドを取得して行う。(GUI アプリとは非同期にする目的が違うため。詳しくは「ASP.NET の非同期/待機の概要」を参照)
-
コンソールアプリでは SynchronizationContext.Current プロパティは null になる(ということは、await で待機する際に「現在の SynchronizationContext をキャプチャ」ということはないはず)。await が完了するとき、スレッドプールを備えた既定の SynchronizationContext を使って、スレッドプールのスレッドで async メソッドの残り処理のスケジュールが設定される。(なので、「await と Task.Result によるデッドロック」に書いたようなデッドロックには陥らない)
async / await を使った非同期プログラミングではプログラマが SynchronizationContext を意識することはほとんどなさそうで、知らなくても済むような気がします。
実際、以下の画像の赤枠部分のコードのように Control.Invoke の代わりに SynchronizationContext.Post メソッドが使えるということぐらいしか使い道は思い当たりません。(自分が知らないだけという可能性は否定できませんが)
MSDN マガジンの記事「並列コンピューティング - SynchronizationContext こそすべて」にも
"開発者を支援するために SynchronizationContext クラスが用意されています。残念なことに、多くの開発者はこの便利なツールに気が付いてすらいません" と書いてありましたが、そうだろうなと思いました。
ただ、async / await を使った非同期プログラミングでも内部的には SynchronizationContext が大きく関与しているのは間違いなく、上記の程度は知っておいて損はないかもしれませんね。