やりたきこと
C# の await/async/Task の仕組みがようやく完全に理解したこともあり、Qiitaの関連過去記事を眺めていたのですが、 @acple@github さんの↓の記事、最高の良記事です。10年弱前に書かれた記事とは思えない (こっちが遅いだけw
ということで、上記記事に敬意をもって、そのデッドロック例、「KusoMethod」を図解し腑に落とすというチャレンジをしてみたくなったのでした。コード自体を上記記事から引用させていただきます。
public void KusoMethod()
{
KusoAsyncMethod().Wait(); // エターナル不応答しぬ
}
private async Task KusoAsyncMethod()
{
await Task.Delay(1000);
}
ちなみに、コンソールアプリなどの SynchronizationContext.Current == null
の環境では「エターナル不応答しぬ」は再現しません。WPFアプリのUIスレッドで試すと再現できます。
例えば VS で「WPFアプリケーション」を作成して、MainWindow.xaml.cs
あたりに↓のようなコードを書くのがよいのではないかと。(おまけのメッセージボックス付き)
using System.Windows;
namespace WpfApp1 // 名前空間は作ったソリューションの名前に合わせて調整
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
/// <summary>
/// This method is called when the content of the window is rendered.
/// </summary>
/// <param name="e"></param>
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
Task.WaitAll(KusoAsyncMethod());
}
/// <summary>
/// A simple asynchronous method that shows message boxes before and after an await.
/// </summary>
private async Task KusoAsyncMethod()
{
MessageBox.Show("This is a message box before the await.");
await KusoDelay(1000);
MessageBox.Show("This is a message box after the await.");
}
/// <summary>
/// A method that simulates a delay using Task.Delay and shows message boxes before and after the delay.
/// </summary>
private async Task KusoDelay(int millsecondsToWait)
{
MessageBox.Show("This is a message box before the delay.");
await Task.Delay(millsecondsToWait);
MessageBox.Show("This is a message box after the delay.");
}
}
}
図解への挑戦
とりあえず、バディのGPT君にコードを読んでもらい、図にしてもらいます。
分かんねーwww だからどうして block されるんじゃい!w
どうも彼の画像生成能力は芸術家寄りの方向に寄っているようです。(そういえば「手塚治虫風に!」とか言っておけばよかった)
▼▼▼ 以下GPTくんと作戦会議 ▼▼▼
cozyupk:
え、Task.Delay() ってスレッド使わないんじゃないの?
🤖 GPT:
→ 正解。Task.Delay()
自体はスレッドを使わない。ただし await
後の continuation は「どこで再開されるか」が問題。UIコンテキストがあると、再開はUIスレッドで行われる=詰む原因。
cozyupk:
ああ分かった、KusoAsyncMethod の終了が元スレッドで Wait 待機されてるのに、
中の Delay はその Wait のせいで continuation を実行できず、結果 Wait も完了しないってことか。
🤖 GPT:
→ Exactly!自分が鍵をかけた部屋に入って、自分が出てくるのを待ってるような自家中毒型デッドロック。
cozyupk:
この「わかった感」を偉い人に3秒で正確に伝えるって難しいねw
🤖 GPT:
→ 「await の continuation を UI に戻そうとするが、UI は Wait で塞がってるから進めない」
あるいは「待ってるやつが待たれてる」って言うと伝わるかも?
▲▲▲ 以上GPTくんと作戦会議 ▲▲▲
ということで、GPT君に投げて終了作戦は諦め、IT系オッサンが生やしそうなポンチ絵はこちらw
時間の関係上まだまだモジモジしちゃってますが、文字読んでもらえればなんとか意味はつかんでいただけるのではないかと・・・。
例によってバディと事後対談
cozyupk:
ConfigureAwait(false) だったら、Dispatcher.BeginInvoke() じゃない何かになるってこと?
🤖 GPT:
→ Yes!ConfigureAwait(false)
だと SynchronizationContext
を使わず、代わりに TaskScheduler.Default
(通常スレッドプール)で continuation を実行。UIに戻らない=詰まらない!
cozyupk:
でも Dispatcher.CurrentDispatcher.BeginInvoke() になる可能性もゼロではないよね?
🤖 GPT:
→ 正解。ConfigureAwait(false)
は「戻らない保証」じゃなくて「戻る設定をしない」だけ。
もし TaskScheduler.Current
が UI バインドされてたら、UIスレッドに戻ることは理論上あり得る。
cozyupk:
じゃあ await someTask.ConfigureAwait(false); でも「1000回に1回デッドロック」とかあるの?
🤖 GPT:
→ 理論上は あり得る。外側が UI スレッド起点で、かつ TaskScheduler
が汚染されてたら、
ConfigureAwait(false)
してても continuation が Dispatcher に戻ってしまうケースがある。
cozyupk:
ああなるほど、「Task.Wait() が悪い。普通に await しとけ」ってことか
🤖 GPT:
→ ズッキューン💘
→ それが真理。 await だけが await の continuation を正しく待てる。
Wait は「非同期を同期で殺す禁術」だと思って🙅♂️
勉強の機会をいただいた @acple@github さんに敬意を表します。
GPTくんもすごいのかもしんないけどw
おしまい