やりたいこと
Taskとか非同期処理を調べていると、あちこちに「コンテキスト」「同期コンテキスト」「非同期コンテキスト」という言葉が出てくる。
これが、調べても調べても何のことを言っているのかわからない。
なんとなく、「コンテキスト ≒ スレッド」なのかな?と感じつつも、説明文の「コンテキスト」を「スレッド」に置き換えて読んでみてみ意味がとおらない。
コンテキストとは何なのか?調べたい。もう調べたのだが、いまだよくわからないので、真実が知りたい。
やったこと
いろいろ調べているうちに、 たまたま、下記のページに出会うことができた。とくに、「いまさら async/await」「async/await と SynchronizationContext 」を拝見して、長年の疑問がすーっと消えていった気がする。
いまさら async/await
https://tech.blog.aerie.jp/entry/2015/08/29/022932
async/await と SynchronizationContext (1)
https://tech.blog.aerie.jp/entry/2015/09/17/110258
async/await と SynchronizationContext (2)
https://tech.blog.aerie.jp/entry/2015/09/17/110425
async/await ~非同期なライブラリは楽じゃない~
https://qwerty2501.hatenablog.com/entry/2014/04/24/235849
(C#) 同期コンテキストと Task, async/await
https://qiita.com/ikorin24/items/92ea46374642a0b59011
まだ100%理解したとは言い難いが、頭の中に浮かんできた現状の理解を、とにかく忘れるまにメモする。
メモ
上のページをみればわかる、といえば一言で終わってしまうので、 現状の理解を言語化してみる。(間違い多々あるかも)
SynchronizationContextについて
- .NET2.0から
SynchronizationContext
クラスが登場した。それが同期コンテキストと呼ばれる場合がある。- 「コンテキスト」とは非同期操作の実行状況や環境に関する情報を含むもの。(by chatGPT)
- SynchronizationContext は、スレッド毎にインスタンスを持っている。
- WPFの場合、UIスレッドは持ってる。ワーカースレッドはもってない。
SynchronizationContext
を使うと、立ち上げたワーカースレッドが実行されるスレッドを、立ち上げた側のスレッドと同じにできたりする。- WPFの場合、ワーカースレッドが実行されるスレッドを、UIスレッドと同じにしたりできる。
- WPFのUIスレッドが自分の
SynchronizationContext
を取得しようと思うと、SynchronizationContext.Current
から取得できる。- ワーカースレッドでは、
SynchronizationContext.Current
にはなにも入っていない。
- ワーカースレッドでは、
- WPFではそういう感じだが、別のプラットフォーム、フレームワーク上では、全く別だったりする。(例えばASP.NET)
- ただ、.NET6時点のTaskやasync/awaitを利用した非同期処理では、直接的に
SynchronizationContext
を見る機会がない。(使わなくても十分非同期処理できる)
- .NET2.0から
Taskについて
- Taskは、Awaitebleオブジェクトである。
- Taskは、awaitすると、
- Taskの処理をしている間は別スレッド(ワーカースレッド)で処理を行って、→①
- 呼び出し元スレッドに一旦戻り、→②
- 別スレの処理が終わったら、呼び出し元の処理の続きを呼び出し元スレッドで実行する。→③
- その動きは、コンパイラが下記のように、処理を分解してくれて実現されてる。
- タスクに登録された処理を別スレッドで実行する。→①に該当
- TaskAwaiterを取得 → ★???
- 自分自身(呼び出し元)の処理のawait以降をコールバックに登録して、いったん処理を終わる →②に該当
- 自分自身を、await前と後に分解して、後の方をコールバックに登録する
- タスクの処理が終わったら、登録したコールバック(自分自身の続き)を実行する →③に該当
TaskAwaiterについて
- TaskAwaiterは、Taskの呼び出し元のコンテキストをキャプチャーして、
- コンテキスト(SynchronizationContext)をキャプチャーする≒そのコンテキストがどのスレッドで行われているか、を覚えておくようなもの
- Task処理が終わったら、
SynchronizationContext.Post
経由でコールバック(=呼び出し元処理の続き)を実行する- キャプチャーしたコンテキストの
SynchronizationContext.Post
経由で呼び出し元の処理を呼ぶということは、つまり呼び出し元のスレッドに戻るということ
- キャプチャーしたコンテキストの
- という感じで、awaitの動きを実現している。
- つまり上の★???でTaskAwaiterを取得したときに、呼び出し元のコンテキストを覚えてるから、Taskの処理が終わった後に、呼び出し元に戻ることができる。
- TaskAwaiterは、Taskの呼び出し元のコンテキストをキャプチャーして、
SynchronizationContext の使いどころはどこか?
.configureAwait(false)
は何をしてるか?- awaitしたあと、元のスレッドに戻る動きをやめて、ワーカースレッドのほうで、await後の処理を進めるようにするということ。
- なので、TaskをWaitしてUIスレッドを止めたあとに、Taskの処理終了時に、止まってるUIスレッドに帰ろうとすることが原因のデッドロックは起きなくなる。(UIスレッドに帰らずに、ワーカースレッドでそのまま動くから。)
結論
今のところ、「処理を実行するスレッドを操るためのもの」と理解。
備考
まとまらないが、ざっと現状理解を言葉にしてみた。
参考
上で紹介させていただいたいくつかのページ。
ありがとうございました。