「Task.FromResult<T> の Result を取ると UI が固まるのでは?」——.NET の非同期を学ぶと必ず出会う疑問です。本記事は、その答えを一刀両断にしつつ、Task.Run や await との違い、UI スレッド/ASP.NET 環境でのデッドロックの仕組み、実戦での使い所・避け所までを、コードと表で徹底的に解説します。
Task.FromResult はメインスレッドをブロックするか
結論:Task.FromResult が返すタスクは既に完了済み(IsCompleted == true/Status == RanToCompletion)のため、Result の取得でブロックは発生しません。
ブロックが起きるかどうかを決めるのは「タスクが完了しているか」それだけです。未完了のタスクに対して Result を読む、あるいは Wait() する場合に限り、完了まで呼び出しスレッドは待機します。Task.FromResult は非同期処理を走らせず、値を保持した「完了済みタスク」オブジェクトを生成して返すだけなので、待つ理由がありません。
| 要点 | 説明 |
|---|---|
| 完了済みタスクを返す | Task.FromResult はスレッドプールにもディスパッチせず、その場で完了状態の Task<T> を返します。 |
Result の一般挙動 | 未完了なら同期待機、完了済みなら即時戻り。FromResult の戻り値は常に後者です。 |
| 例外伝播の違い | Result/Wait は例外を AggregateException に包みます。await は元の例外をそのまま再スローします。 |
| 使い所 | 「すでに計算済みの値」を Task として返したいとき(API の整合、テストのモックなど)。 |
| 避け所 | 重い計算・I/O を見せかけで「同期返し」したいとき。呼び出し側の期待とズレます。 |
サンプル:ブロックしないケース
// 値はすでに手元にある
var completed = Task.FromResult("Hello FromResult");
// すでに完了済みなのでブロックなし
string message = completed.Result; // 即時取得
Console.WriteLine(message); // Hello FromResult
サンプル:ブロックするケース(未完了タスク)
var delayed = Task.Run(async () =>
{
await Task.Delay(1000);
return "Done";
});
// まだ完了していないタイミングで Result を読むと待機する
string result = delayed.Result; // 約 1 秒ブロック
他のタスク生成メソッドとの違い
| API | 実行モデル | 完了前に Result を読むと? | 主用途 |
|---|---|---|---|
Task.FromResult(value) | 同期に「完了済みタスク」を生成 | ブロックしない | 即時の値を Task 型で返す |
Task.CompletedTask | 値なしの完了済みタスク | Wait しても即時戻る | Task 返却の API で即時完了を表現 |
Task.Run(work) | スレッドプールで非同期実行 | 完了までブロック | CPU バウンド処理の並列化 |
async メソッド | await で非同期に継続 | 完了までブロック | I/O バウンド処理、UI 応答性の維持 |
Task.FromException(ex), Task.FromCanceled(ct) | 例外/キャンセル状態の完了済みタスク | Result/Wait で即時に例外 | エラーやキャンセルを同期に伝える |
ValueTask<T> | 完了済みのとき低コストに返せる構造体 | 完了済みならブロックしない | 高頻度・低遅延 API の割当削減 |
なぜ UI が固まることがあるのか:デッドロックのメカニズム
Task.FromResult 自体は安全でも、「未完了の Task に対して Result/Wait を UI スレッドで呼ぶ」と固まることがあります。これは 同期コンテキスト(SynchronizationContext) と 継続のポスト先 が関係します。
- UI スレッド(WPF/WinForms)は
SynchronizationContextを持ち、awaitは既定で「元のコンテキストに戻って続き(継続)を実行」します。 - 呼び出し側が UI スレッドで
task.Resultを呼ぶと、そのスレッドは待機に入り、メッセージループが回らず コンテキストが空かない 状態になります。 - 非同期処理が完了しても、
awaitの継続は「UI コンテキストに戻りたい」。しかし UI はResultによりブロック中。相互待機=デッドロック になります。
典型例(UI スレッドでのデッドロック)
public async Task<string> GetAsync()
{
await Task.Delay(1000); // ここで UI コンテキストをキャプチャ
return "OK";
}
// UI スレッド上(WPF/WinForms のイベントハンドラなど)
string text = GetAsync().Result; // ここで相互待機が発生しうる
回避策
- 「async は async で貫く」(async all the way)。戻り値を
Task/Task<T>にし、呼び出し元もawaitで待つ。 - ライブラリ側で継続のコンテキスト復帰を不要化:内部の
awaitにConfigureAwait(false)を付ける(UI 更新などコンテキスト依存の必要がない箇所に限る)。 - UI スレッドで
Result/Waitを基本禁止。必要ならバックグラウンドにオフロードしてawaitする。
ASP.NET / ASP.NET Core の違い
- ASP.NET(旧):UI と同様に同期コンテキストがあり、
Result/Waitでデッドロックが起きやすい。 - ASP.NET Core:既定では同期コンテキストなし(継続はスレッドプール)。デッドロック確率は低いが、同期ブロックはスレッドプール枯渇(スループット劣化)を招くためアンチパターンです。
FromResult を使うべきシーン/避けるべきシーン
| シーン | 使うべき理由 | サンプル |
|---|---|---|
| キャッシュ済み値の返却 | すでに答えがあり非同期実行は不要 | return Task.FromResult(cache[key]); |
| 機能フラグ/簡易バリデーション結果 | 即時判定できるブールや列挙値の返却 | return Task.FromResult(isEnabled); |
| テストのモック | I/O を行わない偽物のリポジトリ | mock.Setup(x => x.GetAsync()).Returns(Task.FromResult(dto)); |
| 複合パイプラインでの早期終了 | 条件未達のとき即時に空結果を返す | return Task.FromResult(Enumerable.Empty<T>()); |
| 避ける:重い計算・I/O の代替 | 「見せかけの非同期」になり設計が歪む | 本来は await で非同期化すべき |
例外・キャンセルと From 系 API の整理
同期に完了した 失敗 や キャンセル を表す場合は、対になるファクトリを使うと意図が明確になります。
Task.FromException<T>(Exception):失敗で完了したTask<T>Task.FromCanceled<T>(CancellationToken):キャンセルで完了したTask<T>- 値なし版はそれぞれ型推論なしの
Task用オーバーロードあり
なお、Result/Wait は AggregateException で包みます。await は元例外を再スローするため、例外の扱いも await が自然です。
ValueTask と FromResult の関係
高頻度呼び出しで「多くのケースが同期に完了する」API(例:メモリキャッシュ命中)では、Task.FromResult より ValueTask<T> が割り当て削減に有利な場合があります。
- 利点:完了済みなら構造体 1 つで返せるため割り当てを抑制。
- 注意:
ValueTaskは 1 回しかawaitできない、AsTask()のコストなど、呼び出し側の誤用リスクが増える。
公共 API ではまず Task<T> を返し、明確なボトルネックが測定されたときのみ ValueTask<T> を検討するのが無難です。
実践パターン集
API の表面を Task<T> に揃えつつ、即時値を返す
public interface IUserProfileService
{
Task<UserProfile> GetAsync(string id);
}
public sealed class CachedUserProfileService : IUserProfileService
{
private readonly IUserProfileStore _store;
private readonly MemoryCache _cache = new();
```
public async Task<UserProfile> GetAsync(string id)
{
if (_cache.TryGetValue(id, out var cached))
{
// 非同期は不要。即時完了タスクで返す
return await Task.FromResult(cached);
}
var fresh = await _store.LoadAsync(id);
_cache.Set(id, fresh);
return fresh;
}
```
}
UI スレッドでの安全な呼び出し
// ✅ 良い:イベントハンドラ自体を async 化
private async void Button_Click(object sender, EventArgs e)
{
var data = await _service.GetAsync();
this.label1.Text = data.Title;
}
非同期メソッド内部では ConfigureAwait(false) を適材適所で
public async Task<byte[]> DownloadAsync(Uri uri, CancellationToken ct)
{
// ライブラリ層。UI への復帰は不要
using var client = new HttpClient();
return await client.GetByteArrayAsync(uri, ct).ConfigureAwait(false);
}
Result と GetAwaiter().GetResult() の違い
| 呼び方 | ブロックの有無 | 例外の形 | 使い所 |
|---|---|---|---|
task.Result/task.Wait() | 未完了ならブロック | AggregateException にラップ | 避ける(UI/ASP.NET で特に) |
task.GetAwaiter().GetResult() | 未完了ならブロック | 元の例外をスロー(ラップしない) | 同期境界最小化が必要な箇所で稀に |
await task | スレッドはブロックしない(非同期待機) | 元例外をそのまま伝搬 | 推奨デフォルト |
パフォーマンスと割り当ての観点
- FromResult は「安い」がゼロコストではない:タスクオブジェクトの生成は割り当てを伴います。高頻度なら
ValueTaskやキャッシュ戦略を検討。 - Task.Run の乱用は厳禁:I/O バウンドを
Task.Runで包むと無駄にスレッドを消費。I/O はawaitで。 - 同期ブロックはスループットの敵:サーバーサイドではスレッドプールの枯渇を招きやすく、遅延・タイムアウトの原因になります。
よくある誤解と正しい対処
Q.Task.FromResult を多用すると UI が固まる?
A. 固まりません。固まるのは 未完了のタスクに対する同期ブロック が原因です。
Q. 旧 ASP.NET で Result が使えないのはなぜ?
A. 同期コンテキストと継続の相互待機が起きるためです。await に置き換えるか、ライブラリ内部の await を ConfigureAwait(false) で実装してください。
Q. 小さなユーティリティでも async にすべき?
A. はい。async all the way を原則とし、API 表面が同期しかないときだけ慎重にブロッキングを検討します。
実運用チェックリスト
- 返すタスクは本当に完了済みか(
FromResult/CompletedTask)。 - UI/旧 ASP.NET の同期ブロック禁止を守っているか。
Task.Runを I/O に使っていないか。- ライブラリ内部の
awaitに適切にConfigureAwait(false)を付けているか。 - 高頻度パスにおける割り当てが課題なら
ValueTaskを評価したか。
動作確認スニペット(ブロック時間が 0 に近いことを確かめる)
var sw = Stopwatch.StartNew();
var t = Task.FromResult(12345);
_ = t.Result; // 即時
sw.Stop();
Console.WriteLine(sw.ElapsedTicks); // 非常に小さい値(環境による)
FromResult と CompletedTask の使い分け
- 値を返すなら …
Task.FromResult(value) - 値を返さないなら …
Task.CompletedTask
例外・キャンセルは FromException/FromCanceled を選びましょう。意図がコードに現れ、呼び出し側の扱いも明確になります。
安全なパターンの雛形
// 非同期を貫く面
public async Task<T> FooAsync()
{
// I/O
var data = await _client.GetAsync(...).ConfigureAwait(false);
```
// 同期で分かる結果は FromResult でショートカット
if (TryFastPath(data, out var result))
return await Task.FromResult(result);
return await ProcessAsync(data).ConfigureAwait(false);
```
}
まとめ
Task.FromResultは「完了済みタスク」を返すため、Resultを読んでもブロックしません。- ブロックの有無は「タスクが完了しているか」で決まります。未完了タスクに対する同期ブロックは UI/サーバーの健全性を損ねます。
- UI/旧 ASP.NET では
Result/Waitがデッドロックの温床。async all the way とConfigureAwait(false)を基本戦略に。 Task.Runは CPU バウンド専用。I/O はawait。即時値はFromResult/CompletedTaskで表現。- 性能要件が厳しいホットパスでは
ValueTaskも検討。ただし複雑さを増やすため計測に基づいて慎重に。
補足:簡易リファレンス
| 目的 | 推奨 API | ポイント |
|---|---|---|
| 即時に値を返す | Task.FromResult(value) | ブロックなし、意図が明確 |
| 即時に完了(値なし) | Task.CompletedTask | イベント風の API に最適 |
| 即時の失敗/キャンセル | Task.FromException/Task.FromCanceled | 呼び出し側は await で例外/キャンセルを受け取る |
| CPU バウンドの並列化 | Task.Run | 乱用注意。I/O には使わない |
| I/O バウンドの非同期 | async + await | スレッドを塞がない、可読性が高い |
動作を視覚的に理解する(擬似タイムライン)
// FromResult の場合
[Create Task (Completed)] --(即時)--> [Result 読み取り完了]
// Task.Run の場合
[Enqueue to ThreadPool] --(待機)--> [実行中] --(完了)--> [Result 読み取り再開]
// await の場合(UI)
[Await Start] --(UI 手放す)--> [非同期 I/O] --(コンテキスト復帰)--> [継続実行]
この違いが、FromResult は「そもそも実行待ちが存在しない」ことの核心です。
最後にもう一度だけ要点を。Task.FromResult 自体も、その Result 取得もブロックしません。 ブロックは「未完了のタスクに同期でしがみついた」時にだけ起こります。設計の第一原則は async all the way。即時の答えは FromResult、本当の非同期は await。このシンプルな線引きが、UI の快適さとサーバーの堅牢さを両立させます。

コメント