第39章:非同期コードのリファクタ(async/await・キャンセル・タイムアウト)⚡🧵
この章でできるようになること 🎯✨
async/awaitのコードを 短く・読みやすく 整理できるようになる ✂️📘- キャンセル(
CancellationToken)を「ちゃんと効く」形で最後まで通せるようになる 🛑🧷 - タイムアウトを安全に入れて、固まり事故を防げるようになる ⏳✅
- 例外がごちゃつく非同期コードを「境界で整える」感覚がつく 🚧🧼
まずここ!非同期が読みにくくなる3大原因 😵💫🌀

1) 「待つ場所」が散らかる 🧵
await が増えるほど、処理の流れが 飛び飛び に見えて読みづらい…💦
2) キャンセル・タイムアウトが後付けで壊れる 🛑⏳
途中で CancellationToken を渡し忘れると、
UIのキャンセルボタン押したのに止まらない みたいな事故が起きるよ😇
3) 例外が混ざる 💥
- 通信失敗(
HttpRequestException) - キャンセル(
OperationCanceledException) - タイムアウト(
TimeoutExceptionなど)
全部まとめて catch (Exception) すると、仕様が崩れがち…🥲
非同期リファクタの鉄板ルール 5つ 🧱✨
ルール1:async は最後まで貫く(async all the way)🏃♀️💨
Result / Wait() で待つと、UIが固まる&デッドロックの温床になりやすいよ🚫🧊
→ 入口から出口まで await でつなぐのが基本✅
ルール2:戻り値は Task / Task<T> にする 🎁
async void は イベントハンドラ専用(ボタン押下とか)にするのが安全🙆♀️
ルール3:CancellationToken は「引数で受けて、渡し続ける」🧷➡️➡️➡️
「受ける」だけじゃなくて、内部の待ち(I/OやDelay)に渡すのが大事💡
最近のAPIも CancellationToken や TimeSpan timeout を受け取る形が増えてるよ📈✨ (Microsoft Learn)
ルール4:タイムアウトは “仕組み” を統一する ⏳🧠
CancellationTokenSource.CancelAfter(...)で「一定時間後にキャンセル」する → Microsoftの解説がこれだよ🧷 (Microsoft Learn)Task.WaitAsync(timeout, ct)で「このTaskをタイムアウト付きで待つ」 → .NET 10 のAPIにも載ってる標準手段💎 (Microsoft Learn)
ルール5:外部通信は “回復性” を意識する 🛡️🌐
HTTPは落ちる前提…😇
.NET は Microsoft.Extensions.Http.Resilience で タイムアウト/リトライ/遮断みたいな定番を組みやすくしてるよ🧰✨ (Microsoft Learn)
(中で Polly の考え方ともつながるよ)(pollydocs.org)
キャンセルを「ちゃんと効かせる」最小パターン 🛑🧷
基本形:引数で受けて、awaitするAPIに渡す ✅
public async Task<string> DownloadAsync(string url, CancellationToken ct = default)
{
using var http = new HttpClient();
// ct を渡す!ここが超大事✨
return await http.GetStringAsync(url, ct);
}
UI側:キャンセルボタンで止める例 🖱️🛑
private CancellationTokenSource? _cts;
private async void StartButton_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
try
{
var text = await _service.DownloadAsync("https://example.com", _cts.Token);
MessageBox.Show("完了✨\n" + text[..Math.Min(100, text.Length)]);
}
catch (OperationCanceledException)
{
MessageBox.Show("キャンセルしたよ🛑");
}
}
private void CancelButton_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}
ポイント💡
- キャンセルは例外(
OperationCanceledException)として上がってくるのが普通だよ🙂 catch (Exception)に混ぜないで、まずはOperationCanceledExceptionを分けると読みやすい✨
タイムアウトの入れ方 2通り ⏳✨
1) CancelAfter 方式:時間が来たらキャンセルする ⏰🛑
Microsoftの定番はこれ🧷 (Microsoft Learn)
public async Task<string> DownloadWithTimeoutAsync(
string url,
TimeSpan timeout,
CancellationToken ct = default)
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(timeout); // 指定時間でキャンセル予定🛑
using var http = new HttpClient();
return await http.GetStringAsync(url, timeoutCts.Token);
}
「ユーザーキャンセル」と「タイムアウト」を合体して渡してるのがキモだよ🧠✨
2) WaitAsync 方式:待ちにタイムアウトを付ける ⏳🧵
Task.WaitAsync(TimeSpan, CancellationToken) は .NET 10 のAPIにもある標準手段だよ📘 (Microsoft Learn)
public async Task<string> DownloadWithWaitAsync(
string url,
TimeSpan timeout,
CancellationToken ct = default)
{
using var http = new HttpClient();
var task = http.GetStringAsync(url, ct);
return await task.WaitAsync(timeout, ct); // timeout を過ぎたら TimeoutException になりやすい🕰️
}
使い分けイメージ🌷
- CancelAfter:アプリ全体の「この処理はここまで」っていう“締切”を作りたい時に便利🛑
- WaitAsync:すでにある
Taskに「後からタイムアウト」を付けたい時に便利⏳
例外の交通整理:キャンセルとタイムアウトを分ける 🚦✨
ありがちな悪い例 😱
try
{
await _service.DoAsync();
}
catch (Exception ex)
{
// キャンセルもタイムアウトも通信失敗も全部ここ…🥲
_logger.LogError(ex, "失敗");
}
まずはこの形にするのがおすすめ 🧼✅
try
{
await _service.DoAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
// ユーザーキャンセル🛑
}
catch (TimeoutException)
{
// タイムアウト⏳
}
catch (HttpRequestException ex)
{
// 通信失敗🌐💥
}
※ TimeoutException が出るか、OperationCanceledException に寄るかは方式やAPIによって変わるので、
「アプリの見せ方」として境界で整えるのがコツだよ🚧✨
実践:ごちゃごちゃ async を短くするリファクタ ✂️🧁
Before:責務が混ざってる長い async 😵💫
- 入力チェック
- URL組み立て
- HTTP
- 例外整形
- パース が1メソッドに混在💦
public async Task<decimal> GetPriceAsync(string sku)
{
if (string.IsNullOrWhiteSpace(sku))
throw new ArgumentException("sku is required");
using var http = new HttpClient();
var url = "https://api.example.com/prices/" + sku;
try
{
var json = await http.GetStringAsync(url);
// ここでは雑にパースしてる想定
return decimal.Parse(json);
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to get price: " + sku, ex);
}
}
After:待つ場所とロジックを分離して読みやすく ✨📘
ポイントはこれ👇
- 入口でキャンセル/タイムアウトを受け取る
- I/Oは短いメソッドに閉じ込める
- パースは純粋メソッドにする(テストしやすい) 🧪
public async Task<decimal> GetPriceAsync(
string sku,
TimeSpan timeout,
CancellationToken ct = default)
{
ValidateSku(sku);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(timeout);
var json = await DownloadPriceJsonAsync(sku, timeoutCts.Token);
return ParsePrice(json);
}
private static void ValidateSku(string sku)
{
if (string.IsNullOrWhiteSpace(sku))
throw new ArgumentException("sku is required", nameof(sku));
}
private async Task<string> DownloadPriceJsonAsync(string sku, CancellationToken ct)
{
using var http = new HttpClient();
var url = BuildUrl(sku);
// ct を渡す!🧷
return await http.GetStringAsync(url, ct);
}
private static string BuildUrl(string sku)
=> "https://api.example.com/prices/" + sku;
private static decimal ParsePrice(string json)
=> decimal.Parse(json);
読みどころ👀✨
GetPriceAsyncが「やりたいことの順番」だけになって、物語みたいに読める📖- パースや入力チェックは同期メソッドにして、テストが超ラク🧪💕
5. キャンセル・タイムアウトの流れ🛑⏳⚖️
ミニ演習 📝✨
演習1:キャンセル対応を入れてみよう 🛑🧷
次の条件を満たすように修正してね👇
CancellationToken ct = defaultを public メソッドの引数に追加Task.Delay/HttpClientなどの await に ct を渡すOperationCanceledExceptionを UI か呼び出し元で表示
チェック✅
- 「キャンセル押したら本当に止まる」こと!
演習2:タイムアウトを統一しよう ⏳🧠
CancelAfter方式で「3秒」でタイムアウト- タイムアウトしたら「タイムアウト⏳」と表示
- ユーザーキャンセルは「キャンセル🛑」と表示 (分けられたら勝ち✨)
演習3:asyncメソッドを短くしよう ✂️
長い async を見つけて、次をやってね👇
- 「入力整形」「I/O」「ロジック」「例外整形」を別メソッドへ分離
awaitがあるメソッドを“見える範囲”に減らす(目標:1〜2個)👀✨
テストの超基本:async は async でテストする 🧪💖
[Fact]
public async Task Cancel_Should_Throw_OperationCanceledException()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => _service.GetPriceAsync("ABC", TimeSpan.FromSeconds(10), cts.Token));
}
「キャンセル・タイムアウトは仕様」だから、テストで固定できると安心だよ✅✨
AI拡張の使い方 🤖🧠✨
そのまま使える頼み方例 💬
- 「この
asyncメソッドの責務を 4つに分けて、Extract Methodの候補を出して」✂️ - 「
CancellationTokenを最後まで渡す修正を、差分が小さくなる順に提案して」🧷 - 「
CancelAfterとWaitAsyncのどっちが合う?このコードの“締切”の定義も一緒に考えて」⏳
ルール🛡️
- AIの提案は 1コミット分 に刻む🌿
- 置き換えたら 必ず動作確認&テスト ✅
- キャンセル/タイムアウトは 実際に押して試す 🖱️🛑
よくある落とし穴まとめ 💣😇
Task.Result/Wait():UI固まり・デッドロックの元🚫🧊async voidを増やす:例外が捕まえにくい⚠️CancellationTokenを“受けるだけ”で渡さない:キャンセルが効かない🧷💦- タイムアウトの実装がバラバラ:挙動が読めなくなる⏳🌀
- 外部通信でリトライを雑に自作:事故りやすいので標準/定番を活用🛡️ (Microsoft Learn)
まとめ 🌈✨
非同期のリファクタは、やることがシンプルだよ😊💕
- 待つ場所を減らす(短くする)✂️
- キャンセルを最後まで通す 🧷
- タイムアウトを統一する ⏳
- 例外を境界で整理する 🚧
そして、C# 14 / .NET 10 は最新の機能も揃ってるから、安心してこの型で整えていけるよ🌟 (Microsoft Learn)