1. はじめに
C#の非同期は強力なツールだ。でも時々、非同期コードを同期から呼ぶ(あるいはその逆)必要が出てくる。全部が「魔法のように」動くはずに見えても、実際には変なハング(deadlock)、性能低下、さらには予期せぬUIの破壊が起こることがある。しかも多くの場合、バグは実データ上、つまり本番やユーザー環境でだけ現れる。原因はたいてい、同期と非同期の相互作用の誤りや、.NETのタスクスケジューラの挙動に関する微妙な点にあるんだ。
最近のライブラリやフレームワークは非同期メソッドを多用しているから、既存の同期チェーンに非同期を「はめ込む」方法や、その逆で非同期メソッドから同期コードを正しく呼ぶ方法を知っておく必要があるよ。
簡単なおさらい: await 時に何が起きるか
あなたがこう書くと:
await SomeAsyncMethod();
コードは「2つに分断」される:await の前と後。前半は最初の待ちの非同期アクションが始まるまで実行され(例えばネットワークリクエストまで)、続きはその終了後に実行される。問題は:その「続き」はどこで実行されるのか?同じスレッド?別のスレッド?デスクトップアプリ(例えば WPF や WinForms)の場合は?コンソールだと?答えは「場合による」。ここで ConfigureAwait の出番になるんだ。
SynchronizationContext とは何か?
SynchronizationContext は .NET の仕組みで、非同期操作の続きがどこでどのように呼ばれるべきかを「覚えておく」ためのものだよ。
- 従来の WinForms/WPF アプリでは、SynchronizationContext は await の後に残りのコードが UI と同じスレッドで続行されることを保証して、"別スレッドからのコントロールアクセス" エラーを防ぐ。
- 古い ASP.NET(Core ではない)では、SynchronizationContext が HttpContext を復元して HTTP リクエストの処理を続けるのに役立つ。
- コンソールや ASP.NET Core では、たいてい SynchronizationContext は存在しない(null)ので、続きはスレッドプール上で実行されることが多い。
TaskScheduler とは?
TaskScheduler はもう少し低レベルの仕組み。普通は TaskScheduler.Default を使っていて、これは .NET のスレッドプールを使う。どのタスクがいつどこで実行されるかを管理しているんだ。
2. await と Result/Wait() を混ぜたときの deadlock
C#でよくある罠の一つ:
// UIコードのどこかで
var result = SomeAsyncMethod().Result;
あるいは
SomeAsyncMethod().Wait();
アプリがハングした:なぜ?
どうしてそうなるの?
- 非同期メソッドを呼んですぐ .Result や .Wait() を要求すると、現在のスレッドをブロックして非同期タスクの完了を待つことになる。
- SomeAsyncMethod の中で await があり、続きは同じスレッドで SynchronizationContext を経由して実行するように予定される。でもそのスレッドは既に Result/Wait() の待ちでブロックされている。
- スレッドが Result/Wait() を待っているため、continuation(続き)を実行できない。
- これで deadlock:スレッドが自分自身を待っている状態になる。
UIアプリでは特に再現しやすい。UIの全コードが1つのスレッドで動いていて、続きがそのスレッドを待っているからだ。コンソールアプリや ASP.NET Core(同期コンテキストがない)では、こうした deadlock はあまり起きない。
実体験ジョーク: もし .Result で deadlock を踏めたなら、おめでとう、Senior に一歩近づいたってことだよ :D
3. 同期内に非同期を組み込む正しい方法
推奨その1: 非同期は上から下へ
非同期メソッドが出てきたら、async/await を呼び出し元のスタック全体に伝搬させて、UIやエントリポイントまで持っていくのがベスト。中途半端に非同期性を止めないで。
ダメ(スレッドをブロックする):
// 同期メソッドが .Result で非同期を呼んでいる
public void DoStuff()
{
var data = GetDataAsync().Result;
// ...
}
良い(非同期が貫通している):
public async Task DoStuffAsync()
{
var data = await GetDataAsync();
// ...
}
可能なら常に await を使って、.Result / .Wait() は避けよう。
4. でも、どうしても async を sync から呼ぶ必要がある場合
上側のツリーをすべて async に書き換える(可能ならこれが一番良い選択肢)。
特別なパターンを使う:例えば、Task.Run で別スレッド上にタスクを投げ、その中で async メソッドを呼び出す方法。
public void DoStuff()
{
var result = Task.Run(() => SomeAsyncMethod()).Result;
}
ただし:UIアプリではここにも同期の問題や注意点があるから、よほどの事情がない限り避けたほうがいいよ。
5. ConfigureAwait(false) は何をする?
場合によっては、await の後でコードを元のスレッドで続ける必要がないことがある(例えばサーバー側や、UIに依存しないライブラリ内)。むしろ .NET に特定スレッドへの拘束をさせない方が高速になることが多い。
構文と動作の原理
await SomeAsyncMethod().ConfigureAwait(false);
- ConfigureAwait(false) は「元の SynchronizationContext が要らない、どこで続行しても構わないよ」と宣言するもの。
- ConfigureAwait(true)(デフォルト)は「呼んだ場所と同じところで続けてね、できれば同じ SynchronizationContext」という意味だ。
ビジュアル図
┌─────────────────────────────────────────────────────┐
│ SynchronizationContext │
└─────────────────────────────────────────────────────┘
↑ ↑
(UIスレッド) await SomeAsyncMethod() Continuation (await の後)
──────────────────────────────> (同じスレッド — ConfigureAwait(true) の場合)
↓
(任意のスレッド — ConfigureAwait(false) の場合)
ConfigureAwait(false) の使用例 — ライブラリコード向け
ライブラリを書いていて、WinForms, WPF, ASP.NET, コンソールなどいろんな環境で使われる可能性があると想像してみて。あなたのコードはそれらのスレッドモデルに依存すべきじゃない。だからライブラリ内の非同期メソッドでは ConfigureAwait(false) を使うのが良い:
public async Task<string> LoadDataFromUrlAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url).ConfigureAwait(false);
return content;
}
こうすればメソッドは特定の SynchronizationContext を「要求」しなくなる。安全で、スイッチングが減ってパフォーマンス向上にもつながることが多いよ。
await に ConfigureAwait を付けない場合に何が起きるか — 例
WPFアプリを考えてみよう:
private async void Button_Click(object sender, RoutedEventArgs e)
{
Button1.Content = "読み込み中...";
await Task.Delay(2000); // 長い処理の模擬
Button1.Content = "完了!";
}
Task.Delay の中では内部的に await を行う。デフォルトでは await の後、制御は UI スレッドに戻るので UI 要素の更新が安全に行えるよ。
もし長い処理の中で ConfigureAwait(false) を使うと:
await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "完了!"; // エラーになる!
例外が発生する:InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
理由は、続きが別スレッドで実行されるため UI にアクセスできないからだ。
結論:UI やコンテキストが必要な場所では ConfigureAwait(false) を使わないでね。
6. 便利な細かいポイント
どこでいつ ConfigureAwait を使うか
| シナリオ | .ConfigureAwait(false) を使うべき? | 理由 |
|---|---|---|
| ライブラリコード | はい | どんなコンテキストから呼ばれるかわからないし、UIは不要 |
| ASP.NET Core 内のコード | はい(ほとんどコンテキストがない) | パフォーマンスが向上する |
| WinForms/WPF で UI に触る場合 | いいえ | UI スレッドに制御を戻す必要がある |
| 同期メソッド内 | 意味がない | そもそも同期コンテキストがない |
| コンソールアプリ | 使っても良いが効果は目に見えない | コンテキストがない |
SynchronizationContext があるかどうかをどう調べるか
コードから直接チェックできるよ:
Console.WriteLine(SynchronizationContext.Current == null
? "コンテキストなし"
: "コンテキストあり");
- コンソールや ASP.NET Core アプリでは "コンテキストなし" になる。
- WinForms/WPF では "コンテキストあり" になる。
スレッド遷移の可視化
sequenceDiagram
participant MainThread as メインスレッド(UI/Console)
participant ThreadPool as プールからのスレッド
MainThread->>SomeAsyncMethod: メソッド呼び出し
SomeAsyncMethod->>MainThread: await (ConfigureAwaitなし)
Note right of MainThread: await の後、同じスレッドに戻る
SomeAsyncMethod->>ThreadPool: await (ConfigureAwait(false))
Note right of ThreadPool: await の後、どのスレッドでも動ける
使い方の短いルール
- UIコード内で非同期メソッドに対して .Result や .Wait() を使わないこと。
- ライブラリコードではすべての await に対して原則として ConfigureAwait(false) を使うこと。
- UIアプリでは UI に触るメソッド以外の内部でだけ ConfigureAwait(false) を使うこと。
- 同期と非同期をむやみに混ぜないこと(必要がない限り)。
- async を sync から呼ぶ必要があるなら、本当にチェーン全体を async にできないか再考して。
7. 非同期を扱うときの初心者の典型ミス
ミスその1: 無差別に .Result や .Wait() を使うこと。
特に記事を読んだ後の開発者が、非同期呼び出しを同期化するためにあちこちで .Result や .Wait() を付けたがるんだ。見た目では便利そうだけど、実際には UI アプリで deadlock に直行する最短ルート。非同期メソッド内で ConfigureAwait(false) が使われていないと、UIスレッドがブロックされて続きが実行できなくなるから特に危険だよ。
ミスその2: ConfigureAwait(false) の誤用や乱用。
初心者は ConfigureAwait(false) をどこでも使ってしまいがちで、UIに触るコードにも入れてしまう。UIアプリではこれが原因でコントロールアクセスの例外(InvalidOperationException)を引き起こす。続きが UI とは別スレッドで実行されるからだ。
ミスその3: ConfigureAwait が awaitable のみで動くことを忘れること。
多くの人は ConfigureAwait(false) をどんなメソッドでも使えると思っているけど、実際には Task, Task<T>, ValueTask などの awaitable 型でしか意味を持たない。同期メソッドには効果がないし、同期的な待ちが非同期になるわけでもない。
ミスその4: 必要もないのに同期と非同期を混ぜること。
非同期を同期から呼ぶために無計画に .Result、.Wait()、あるいは Task.Run を使うと、微妙なバグ、性能悪化、診断の難しい deadlock を招く。短く安全そうに見えても、チェーン全体で問題を引き起こすことがある。
ミスその5: SynchronizationContext の影響を甘く見ること。
初心者は await の後の続きが同じスレッド(UI)で実行されることを忘れがちだ。ConfigureAwait(false) を使わないと続きが UI スレッドで動く可能性がある、という点を意識していないと、UI とライブラリコードの組み合わせで予測不能な挙動になることがあるよ。
GO TO FULL VERSION