C# async/await入門|非同期処理・Taskの使い方とつまずきやすい注意点をわかりやすく解説
はじめに
C#でアプリケーションを開発していると、ファイルの読み書き、Web APIへのアクセス、データベース処理、画面操作など、処理の完了を待つ場面がよくあります。こうした待ち時間をうまく扱うために使われるのが、C#のasync/awaitです。
async/awaitを使うと、時間のかかる処理を待っている間もアプリケーション全体を止めずに済みます。たとえば、画面が固まらないUIアプリを作ったり、Webサーバーで多数のリクエストを効率よく処理したりできます。
一方で、async/awaitは見た目がシンプルな分、「asyncを付ければ別スレッドになるのか」「awaitを書き忘れるとどうなるのか」「Task.Runはいつ使うのか」など、初心者がつまずきやすいポイントも多くあります。
この記事では、C#のasync/awaitとTaskの基本から、実践的な使い方、エラー処理、キャンセル処理、ベストプラクティスまでを順番に解説します。
1. C#のasync/awaitとは?まず押さえる非同期処理の基本
1-1. 非同期処理とは何か
非同期処理とは、時間のかかる処理の完了を待っている間に、呼び出し元の処理をブロックしない仕組みです。
たとえば、Web APIからデータを取得する処理を考えてみます。通信には数百ミリ秒から数秒かかることがあります。この間、何もせずにスレッドを止めてしまうと、UIアプリなら画面が固まり、Webアプリなら他のリクエストを処理しにくくなります。
非同期処理を使うと、通信やファイルI/Oなどの完了を待っている間、スレッドを別の仕事に使えるようになります。これにより、アプリケーションの応答性やスケーラビリティを高めることができます。
C#では、この非同期処理を扱いやすくするためにasync/awaitが用意されています。
1-2. 同期処理と非同期処理の違い
同期処理では、ある処理が終わるまで次の処理に進みません。
C#var result = GetData();
Console.WriteLine(result);
この例では、GetData()が完了するまでConsole.WriteLineには進みません。処理が単純で短時間なら問題ありませんが、通信やファイル操作のように待ち時間が長い処理では、アプリ全体の動きが悪くなります。
一方、非同期処理では、時間のかかる処理を待っている間に呼び出し元のスレッドを解放できます。
C#var result = await GetDataAsync();
Console.WriteLine(result);
awaitを書くと、GetDataAsync()の完了を待ちます。ただし、同期処理のようにスレッドを占有して待つのではなく、いったん呼び出し元に制御を返します。処理が完了すると、その続きから再開されます。
つまり、同期処理は「終わるまでその場で待つ」、非同期処理は「終わったら続きを実行する」というイメージです。
1-3. async/awaitを使うメリット
async/awaitの大きなメリットは、非同期処理を同期処理に近い読みやすい形で書けることです。
async/awaitを使わない場合、非同期処理の完了後に実行したい処理をコールバックや継続処理として書く必要があり、コードが複雑になりがちです。
async/awaitを使うと、次のように自然な流れで書けます。
C#public async Task ShowDataAsync()
{
var data = await GetDataAsync();
Console.WriteLine(data);
}
このコードは、GetDataAsync()の完了を待ってから結果を表示します。見た目は上から下に流れる通常のコードとほとんど変わりません。
主なメリットは次のとおりです。
UIアプリで画面が固まりにくくなる
Webアプリでスレッドを効率よく使える
非同期処理を読みやすく書ける
例外処理を
try-catchで自然に書ける複数の処理を並列に待つコードを書きやすい
1-4. async/awaitは「別スレッドで実行する仕組み」ではない
async/awaitを理解するうえで重要なのが、「async/awaitは必ず別スレッドで処理する仕組みではない」という点です。
初心者のうちは、asyncを付けると自動的に別スレッドで動くと思いがちです。しかし、実際にはそうではありません。
async/awaitは、主に「待ち時間のある処理を効率よく扱うための構文」です。特にWeb API呼び出し、ファイルI/O、データベースアクセスなどのI/O待ちでは、処理の完了を待っている間にスレッドを解放できます。
一方、重い計算処理のようにCPUを使い続ける処理は、async/awaitを付けるだけでは軽くなりません。そのような処理を別スレッドで実行したい場合は、Task.Runなどを使ってスレッドプール上で実行する必要があります。
C#var result = await Task.Run(() => HeavyCalculation());
つまり、async/awaitは「非同期処理を扱いやすくする構文」であり、「自動的に別スレッド化する魔法」ではありません。
1-5. C#で非同期処理がよく使われる場面
C#のasync/awaitは、さまざまな場面で使われます。特によく使われるのは、待ち時間が発生しやすいI/O処理です。
たとえば、次のような処理です。
Web APIへのHTTPリクエスト
ファイルの読み書き
データベースアクセス
クラウドストレージへのアクセス
メール送信
UIアプリでのボタンクリック処理
Webアプリでのリクエスト処理
たとえば、HttpClientでWeb APIを呼び出す場合は、次のようにawaitを使います。
C#using var client = new HttpClient();
string json = await client.GetStringAsync("https://example.com/api/items");
Console.WriteLine(json);
このように、C#の現代的なライブラリでは、Asyncで終わる非同期メソッドが多く提供されています。C#で実用的なアプリケーションを書くなら、async/awaitは避けて通れない基本知識です。
2. async/awaitとTaskの関係をわかりやすく理解する
2-1. asyncキーワードの役割
asyncは、メソッドの中でawaitを使えるようにするためのキーワードです。
C#public async Task DoSomethingAsync()
{
await Task.Delay(1000);
}
この例では、DoSomethingAsyncメソッドにasyncが付いているため、メソッド内でawaitを使えます。
ただし、asyncを付けただけで処理が非同期になるわけではありません。実際に非同期的に待つには、メソッド内でawaitを使ってTaskなどを待つ必要があります。
たとえば、次のコードはasyncが付いていても、実質的には非同期処理をしていません。
C#public async Task DoSomethingAsync()
{
Console.WriteLine("Hello");
}
このようなコードを書くと、コンパイラから「awaitがない」という警告が出ます。asyncはあくまでawaitを使うための準備であり、それ単体で処理を別スレッド化したり、非同期化したりするものではありません。
2-2. awaitキーワードの役割
awaitは、非同期処理の完了を待ち、その結果を取り出すためのキーワードです。
C#string result = await GetMessageAsync();
このコードでは、GetMessageAsync()が返すTask<string>の完了を待ち、完了後にstring型の結果をresultに代入します。
awaitの重要なポイントは、単に「待つ」だけではないことです。awaitは、処理が完了していない場合、現在のメソッドをいったん中断し、呼び出し元に制御を返します。そして、待っていた処理が完了すると、中断した続きから再開します。
次のようなコードがあるとします。
C#Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("終了");
実行すると、「開始」が表示され、約1秒後に「終了」が表示されます。この1秒間、スレッドを占有して止め続けるのではなく、非同期的に待機します。
2-3. Taskとは何か
Taskは、非同期処理を表すオブジェクトです。簡単に言えば、「まだ終わっていないかもしれない処理の結果」を表します。
戻り値のない非同期処理はTaskで表します。
C#public async Task SaveAsync()
{
await Task.Delay(1000);
Console.WriteLine("保存しました");
}
このSaveAsyncメソッドは、完了までに時間がかかる処理を表しています。呼び出し側は、このTaskをawaitすることで完了を待てます。
C#await SaveAsync();
Taskには、処理の状態や完了、キャンセル、例外などの情報が含まれます。非同期処理が失敗した場合、その例外もTaskに保持され、awaitしたときに再スローされます。
2-4. TaskとTask<T>の違い
TaskとTask<T>の違いは、戻り値があるかどうかです。
戻り値がない非同期メソッドはTaskを返します。
C#public async Task SendAsync()
{
await Task.Delay(1000);
Console.WriteLine("送信完了");
}
戻り値がある非同期メソッドはTask<T>を返します。
C#public async Task<int> GetCountAsync()
{
await Task.Delay(1000);
return 10;
}
この場合、呼び出し側でawaitすると、Task<int>からintの結果を取り出せます。
C#int count = await GetCountAsync();
Console.WriteLine(count);
整理すると、次のようになります。
C#Task // 戻り値なしの非同期処理
Task<T> // T型の戻り値がある非同期処理
Task<string>なら最終的にstringを返し、Task<int>なら最終的にintを返します。
2-5. voidではなくTaskを返すべき理由
非同期メソッドでは、基本的にvoidではなくTaskを返すべきです。
悪い例は次のようなコードです。
C#public async void SaveAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}
async voidにしてしまうと、呼び出し側が処理の完了を待てません。また、例外を呼び出し側で捕捉しにくくなります。
一方、Taskを返すようにすれば、呼び出し側でawaitでき、例外もtry-catchで扱えます。
C#public async Task SaveAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}
try
{
await SaveAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
async voidが許される代表的な場面は、UIアプリのイベントハンドラーです。
C#private async void Button_Click(object sender, EventArgs e)
{
await SaveAsync();
}
イベントハンドラーは戻り値の型がvoidと決まっていることが多いため、例外的にasync voidを使います。それ以外の通常の非同期メソッドでは、原則としてTaskまたはTask<T>を返すようにしましょう。
2-6. asyncメソッドの基本的な書き方
非同期メソッドの基本形は次のとおりです。
C#public async Task MethodNameAsync()
{
await SomeAsyncMethod();
}
戻り値がある場合は、次のようにTask<T>を使います。
C#public async Task<string> GetNameAsync()
{
await Task.Delay(1000);
return "Taro";
}
呼び出し側では、awaitを使って結果を受け取ります。
C#string name = await GetNameAsync();
Console.WriteLine(name);
メソッド名の末尾には、慣例としてAsyncを付けます。これは必須ではありませんが、呼び出し側が非同期メソッドだとすぐに分かるため、C#では広く使われている命名ルールです。
3. C# async/awaitの基本的な使い方
3-1. 最小構成のasync/awaitサンプル
まずは、最小構成のasync/awaitの例を見てみましょう。
C#using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("開始");
await WaitAsync();
Console.WriteLine("終了");
}
static async Task WaitAsync()
{
await Task.Delay(1000);
Console.WriteLine("1秒待ちました");
}
}
実行すると、次のような流れになります。
開始
1秒待ちました
終了
Task.Delay(1000)は、1秒待機する非同期処理です。Thread.Sleep(1000)と違い、待っている間にスレッドをブロックしません。
この例では、Mainメソッドにもasyncを付け、await WaitAsync()で非同期メソッドの完了を待っています。
3-2. 戻り値なしの非同期メソッドを書く
戻り値のない非同期メソッドは、Taskを返します。
C#public async Task PrintMessageAsync()
{
await Task.Delay(500);
Console.WriteLine("メッセージを表示しました");
}
呼び出し側は次のように書きます。
C#await PrintMessageAsync();
このとき、PrintMessageAsync()が完了するまで、呼び出し側のメソッドはその位置で中断されます。処理が完了すると、awaitの次の行から再開されます。
戻り値がないからといってvoidにするのではなく、Taskを返す点が重要です。
3-3. 戻り値ありの非同期メソッドを書く
戻り値がある場合は、Task<T>を返します。
C#public async Task<string> GetUserNameAsync()
{
await Task.Delay(500);
return "Yamada";
}
呼び出し側では、awaitによってstring型の結果を受け取れます。
C#string userName = await GetUserNameAsync();
Console.WriteLine(userName);
GetUserNameAsync()自体の戻り値はTask<string>ですが、awaitすると中身のstringを取り出せます。
次のように考えると分かりやすいです。
C#Task<string> task = GetUserNameAsync();
string userName = await task;
Task<T>は「将来T型の値を返す処理」を表す型です。
3-4. awaitで処理の完了を待つ
awaitは、非同期処理の完了を待つために使います。
C#await Task.Delay(1000);
Console.WriteLine("待機完了");
この場合、Task.Delay(1000)が完了してからConsole.WriteLineが実行されます。
ただし、awaitは単に待つだけではなく、例外の受け取りにも関係します。非同期メソッド内で例外が発生した場合、その例外はTaskに格納され、awaitしたタイミングで投げ直されます。
C#try
{
await FailingAsync();
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
そのため、非同期処理の成功・失敗を正しく扱うには、基本的にawaitすることが重要です。
3-5. 複数の非同期処理を順番に実行する
複数の非同期処理を順番に実行したい場合は、単純に上から順にawaitを書きます。
C#await Step1Async();
await Step2Async();
await Step3Async();
この場合、Step1Asyncが終わってからStep2Async、Step2Asyncが終わってからStep3Asyncが実行されます。
たとえば、ログインしてからユーザー情報を取得し、その後に注文履歴を取得するような処理では、順番に実行する必要があります。
C#await LoginAsync();
var user = await GetUserAsync();
var orders = await GetOrdersAsync(user.Id);
依存関係がある処理は、順番にawaitするのが自然です。
一方で、互いに依存しない処理を順番にawaitすると、必要以上に時間がかかることがあります。その場合は、後述するTask.WhenAllを使うと効率的です。
3-6. 非同期メソッドを呼び出す側の書き方
非同期メソッドを呼び出す側も、基本的にはasyncにしてawaitを使います。
C#public async Task ExecuteAsync()
{
await SaveAsync();
}
awaitを使うには、そのメソッド自体にもasyncを付ける必要があります。つまり、非同期処理は呼び出し元へ伝播していくことが多いです。
コンソールアプリでは、Mainメソッドを次のように書けます。
C#static async Task Main(string[] args)
{
await ExecuteAsync();
}
古い書き方ではMainから非同期メソッドを直接awaitできないケースもありましたが、現在のC#ではasync Task Mainを使えるため、コンソールアプリでも自然にasync/awaitを書けます。
4. Taskを使った実践的な非同期処理
4-1. Task.Delayで非同期処理の動きを確認する
Task.Delayは、指定した時間だけ非同期的に待機するメソッドです。サンプルやテストで非同期処理の動きを確認するときによく使われます。
C#public async Task SampleDelayAsync()
{
Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("1秒後");
}
Task.Delayは、実際のアプリケーションでは「一定時間待ってから処理する」「タイムアウトを表現する」「リトライ間隔を空ける」といった用途にも使えます。
ただし、実際のI/O処理の代わりに使う場合は、あくまでサンプル用だと理解しておきましょう。Web APIやファイル読み込みなどでは、それぞれのライブラリが提供しているAsyncメソッドを使います。
4-2. Task.Runで重い処理をバックグラウンド実行する
Task.Runは、指定した処理をスレッドプール上で実行するために使います。主にCPU負荷の高い処理をUIスレッドから逃がしたい場合などに使います。
C#public async Task<int> CalculateAsync()
{
int result = await Task.Run(() =>
{
int sum = 0;
for (int i = 0; i < 100000000; i++)
{
sum += i;
}
return sum;
});
return result;
}
この例では、重い計算処理をTask.Runの中で実行しています。UIアプリでこのような処理を直接UIスレッド上で実行すると、画面が固まる原因になります。
ただし、Task.Runは何にでも使えばよいわけではありません。Web API呼び出しやデータベースアクセスのようなI/O処理では、通常はライブラリが提供する非同期メソッドをそのままawaitすれば十分です。
C#// 通常はこちらで十分
var json = await client.GetStringAsync(url);
// 不要な例
var json = await Task.Run(() => client.GetStringAsync(url));
I/O待ちにはasync/await、CPU負荷の高い処理には必要に応じてTask.Run、と使い分けることが大切です。
4-3. Task.WhenAllで複数処理を並列に待つ
互いに依存しない複数の非同期処理を同時に開始し、すべての完了を待ちたい場合はTask.WhenAllを使います。
C#Task<string> task1 = GetDataAsync("A");
Task<string> task2 = GetDataAsync("B");
Task<string> task3 = GetDataAsync("C");
string[] results = await Task.WhenAll(task1, task2, task3);
foreach (var result in results)
{
Console.WriteLine(result);
}
このコードでは、3つの非同期処理を先に開始してから、Task.WhenAllですべての完了を待っています。
次のように順番にawaitすると、処理が直列になってしまいます。
C#string result1 = await GetDataAsync("A");
string result2 = await GetDataAsync("B");
string result3 = await GetDataAsync("C");
それぞれの処理が独立している場合は、Task.WhenAllを使った方が全体の待ち時間を短くできることがあります。
ただし、一度に大量の処理を開始すると、外部APIやデータベースに負荷をかける可能性があります。必要に応じて同時実行数を制限する設計も重要です。
4-4. Task.WhenAnyで最初に完了した処理を受け取る
Task.WhenAnyは、複数のTaskのうち、最初に完了したものを受け取るために使います。
C#Task<string> task1 = GetDataAsync("A");
Task<string> task2 = GetDataAsync("B");
Task<string> completedTask = await Task.WhenAny(task1, task2);
string result = await completedTask;
Console.WriteLine(result);
Task.WhenAny自体は、最初に完了したTaskを返します。そのため、結果を取り出すには、返ってきたTaskをさらにawaitします。
よくある用途は、複数の処理のうち最初に終わったものを使うケースや、タイムアウト処理です。
C#Task<string> dataTask = GetDataAsync();
Task timeoutTask = Task.Delay(3000);
Task completed = await Task.WhenAny(dataTask, timeoutTask);
if (completed == timeoutTask)
{
Console.WriteLine("タイムアウトしました");
}
else
{
string data = await dataTask;
Console.WriteLine(data);
}
このように、Task.WhenAnyを使うと、一定時間以内に処理が終わらない場合の制御を書けます。
4-5. IO待ちとCPU負荷の高い処理で使い方を分ける
非同期処理では、I/O待ちとCPU負荷の高い処理を分けて考えることが重要です。
I/O待ちとは、通信、ファイル、データベースなど、外部の応答を待つ処理です。この場合、処理の大部分は「待ち時間」なので、async/awaitによってスレッドを効率よく解放できます。
C#var json = await httpClient.GetStringAsync(url);
一方、CPU負荷の高い処理は、計算や画像処理、圧縮処理など、CPUを使い続ける処理です。この場合、async/awaitを付けるだけでは処理は軽くなりません。
C#var result = await Task.Run(() => HeavyCalculation());
UIアプリでは、重いCPU処理をTask.Runでバックグラウンドに逃がすと、画面のフリーズを防げることがあります。
ただし、ASP.NET Coreのようなサーバーサイドアプリでは、安易にTask.Runを使うとスレッドプールを余計に消費することがあります。サーバー側では、I/O処理は非同期APIをそのままawaitし、CPU負荷の高い処理は設計全体で負荷を考える必要があります。
4-6. Web API呼び出しでasync/awaitを使う例
実際のアプリケーションでは、Web API呼び出しでasync/awaitを使う場面が非常に多いです。
C#using System.Net.Http;
using System.Threading.Tasks;
public class ApiClient
{
private readonly HttpClient _httpClient;
public ApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetUsersAsync()
{
string url = "https://example.com/api/users";
string json = await _httpClient.GetStringAsync(url);
return json;
}
}
呼び出し側は次のように書きます。
C#var client = new ApiClient(new HttpClient());
string usersJson = await client.GetUsersAsync();
Console.WriteLine(usersJson);
より実践的には、GetAsyncでレスポンスを取得し、ステータスコードを確認します。
C#public async Task<string> GetUsersAsync()
{
using HttpResponseMessage response =
await _httpClient.GetAsync("https://example.com/api/users");
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
return json;
}
EnsureSuccessStatusCodeは、HTTPステータスコードが成功でない場合に例外を投げます。そのため、呼び出し側ではtry-catchでエラーを扱えます。
C#try
{
string json = await client.GetUsersAsync();
Console.WriteLine(json);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
}
5. async/awaitでつまずきやすい注意点
5-1. awaitを書き忘れるとどうなるか
非同期メソッドを呼び出すときにawaitを書き忘れると、処理の完了を待たずに次の行へ進みます。
C#SaveAsync();
Console.WriteLine("保存完了");
このコードでは、SaveAsync()の完了を待たずに「保存完了」と表示される可能性があります。保存処理がまだ終わっていないのに、完了したように見えてしまう危険があります。
正しくは次のように書きます。
C#await SaveAsync();
Console.WriteLine("保存完了");
また、awaitしないTaskで例外が発生すると、呼び出し側のtry-catchで捕捉できません。
C#try
{
SaveAsync(); // awaitしていない
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // ここでは捕捉できない
}
非同期メソッドを呼び出したら、基本的にはawaitするようにしましょう。
5-2. async voidを避けるべき理由
async voidは、呼び出し側が完了を待てず、例外も扱いにくいため、通常は避けるべきです。
C#public async void DoWorkAsync()
{
await Task.Delay(1000);
throw new Exception("失敗しました");
}
このようなメソッドは、呼び出し側で次のようにしても例外を捕捉できません。
C#try
{
DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
async voidではなく、Taskを返すようにします。
C#public async Task DoWorkAsync()
{
await Task.Delay(1000);
throw new Exception("失敗しました");
}
呼び出し側では、awaitして例外を捕捉します。
C#try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
async voidを使ってよい代表例は、UIイベントハンドラーです。それ以外では、TaskまたはTask<T>を返す設計にしましょう。
5-3. Task.WaitやResultでデッドロックが起きる理由
非同期処理を同期的に待つために、Task.Wait()や.Resultを使いたくなることがあります。
C#var result = GetDataAsync().Result;
または次のように書く場合です。
C#GetDataAsync().Wait();
しかし、UIアプリや古いASP.NET環境では、これがデッドロックの原因になることがあります。
理由は、awaitの後の処理が元のコンテキスト、たとえばUIスレッドに戻ろうとする一方で、そのUIスレッドが.Resultや.Wait()でブロックされているためです。
流れを簡単にすると、次のようになります。
UIスレッドで
GetDataAsync().Resultを呼ぶUIスレッドが結果を待ってブロックされる
GetDataAsync内のawait後の処理がUIスレッドに戻ろうとするしかしUIスレッドはブロック中なので続きが実行できない
お互いに待ち合って止まる
このような問題を避けるため、非同期処理は最後までawaitでつなぐのが基本です。
C#var result = await GetDataAsync();
「asyncは呼び出し元まで伝播させる」という考え方が重要です。
5-4. 例外処理はtry-catchでどこに書くべきか
非同期メソッド内で発生した例外は、awaitした場所で受け取れます。
C#public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
throw new Exception("データ取得に失敗しました");
}
呼び出し側では、次のようにtry-catchを書きます。
C#try
{
string data = await GetDataAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
try-catchは、awaitを含む範囲に書くのが基本です。
C#try
{
await Step1Async();
await Step2Async();
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
処理ごとに個別にエラーを扱いたい場合は、それぞれにtry-catchを書きます。
C#try
{
await SaveAsync();
}
catch (Exception ex)
{
Console.WriteLine($"保存エラー: {ex.Message}");
}
try
{
await SendMailAsync();
}
catch (Exception ex)
{
Console.WriteLine($"メール送信エラー: {ex.Message}");
}
どこで捕捉するかは、「そのエラーに対して意味のある対応ができる場所」で考えると分かりやすいです。
5-5. ConfigureAwait(false)はいつ使うべきか
ConfigureAwait(false)は、await後に元のコンテキストへ戻る必要がない場合に使われます。
C#var result = await GetDataAsync().ConfigureAwait(false);
UIアプリでは、awaitの後に画面を更新する場合、UIスレッドに戻る必要があります。
C#var data = await GetDataAsync();
label.Text = data;
このような場合にConfigureAwait(false)を使うと、await後の処理がUIスレッドではない場所で実行される可能性があり、UI操作で問題が起きることがあります。
一方、ライブラリコードでは、特定のUIスレッドや同期コンテキストに戻る必要がないことが多いため、ConfigureAwait(false)が使われることがあります。
C#public async Task<string> LoadTextAsync()
{
return await File.ReadAllTextAsync("sample.txt")
.ConfigureAwait(false);
}
初心者のうちは、アプリケーションコードでは無理に使わなくても構いません。特にUI更新がある場所では慎重に扱いましょう。
5-6. UIアプリでスレッドを意識すべき場面
WPF、Windows Forms、MAUIなどのUIアプリでは、画面の部品は基本的にUIスレッドから操作する必要があります。
たとえば、ボタンをクリックしてデータを取得し、画面に表示する処理を考えます。
C#private async void Button_Click(object sender, EventArgs e)
{
button.Enabled = false;
try
{
string data = await GetDataAsync();
label.Text = data;
}
finally
{
button.Enabled = true;
}
}
この例では、await後にUIスレッドへ戻るため、label.Textを安全に更新できます。
一方、Task.Runの中で直接UIを更新するのは避けるべきです。
C#await Task.Run(() =>
{
// UI部品をここで直接触るのは危険
// label.Text = "完了";
});
重い処理だけをTask.Runに入れ、UI更新はawaitの後に行うのが基本です。
C#var result = await Task.Run(() => HeavyCalculation());
label.Text = result.ToString();
UIアプリでは、「重い処理はUIスレッドから逃がす」「UI更新はUIスレッドで行う」という考え方が重要です。
5-7. 非同期処理を途中でキャンセルする方法
非同期処理を途中でキャンセルしたい場合は、CancellationTokenを使います。
C#public async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(1000, cancellationToken);
Console.WriteLine($"{i + 1}回目");
}
}
呼び出し側では、CancellationTokenSourceを使います。
C#using var cts = new CancellationTokenSource();
Task task = DoWorkAsync(cts.Token);
// 途中でキャンセル
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
キャンセルは、強制的にスレッドを停止する仕組みではありません。処理側がCancellationTokenを確認し、キャンセル要求に協力することで実現します。
そのため、自作の非同期メソッドでは、必要に応じてCancellationTokenを引数に受け取り、適切なタイミングで確認することが大切です。
6. async/awaitのエラー処理とキャンセル処理
6-1. 非同期メソッド内の例外の伝わり方
非同期メソッド内で例外が発生すると、その例外は返されたTaskに記録されます。そして、呼び出し側がawaitしたときに例外として投げ直されます。
C#public async Task<int> GetNumberAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("数値を取得できませんでした");
}
呼び出し側では、通常の同期コードと同じようにtry-catchで扱えます。
C#try
{
int number = await GetNumberAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
async/awaitの良い点は、非同期処理でも例外処理を自然な形で書けることです。
ただし、例外を正しく受け取るにはawaitする必要があります。Taskを放置すると、例外に気づきにくくなります。
6-2. awaitしないTaskの例外に注意する
次のように、非同期メソッドを呼び出してawaitしないコードは注意が必要です。
C#DoWorkAsync();
この場合、DoWorkAsync内で例外が発生しても、呼び出し側はその例外を直接受け取れません。
C#try
{
DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // 捕捉できない
}
正しくは、次のようにawaitします。
C#try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
どうしても「実行するが待たない」処理が必要な場合もありますが、その場合でも例外を内部で処理する設計にすべきです。
C#_ = Task.Run(async () =>
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine($"バックグラウンド処理でエラー: {ex.Message}");
}
});
ただし、このような「fire-and-forget」は管理が難しいため、多用は避けましょう。
6-3. Task.WhenAll実行時の例外処理
Task.WhenAllで複数の非同期処理を待っている場合、いずれかのタスクで例外が発生すると、await Task.WhenAll(...)の箇所で例外が投げられます。
C#try
{
await Task.WhenAll(
Task1Async(),
Task2Async(),
Task3Async()
);
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
複数のタスクが失敗する可能性がある場合、個別の例外を詳しく確認したいことがあります。その場合は、タスクを変数に保持しておくと扱いやすくなります。
C#var tasks = new[]
{
Task1Async(),
Task2Async(),
Task3Async()
};
try
{
await Task.WhenAll(tasks);
}
catch
{
foreach (var task in tasks)
{
if (task.IsFaulted && task.Exception != null)
{
foreach (var ex in task.Exception.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
}
}
Task.WhenAllを使うときは、「どれか1つでも失敗したら全体として失敗扱いにするのか」「失敗した処理だけ記録して他は続けるのか」を事前に設計しておくことが大切です。
6-4. CancellationTokenの基本
CancellationTokenは、非同期処理にキャンセル要求を伝えるための仕組みです。
メソッド側では、引数としてCancellationTokenを受け取ります。
C#public async Task DownloadAsync(CancellationToken cancellationToken)
{
await Task.Delay(3000, cancellationToken);
Console.WriteLine("ダウンロード完了");
}
呼び出し側では、CancellationTokenSourceを作成します。
C#using var cts = new CancellationTokenSource();
try
{
await DownloadAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました");
}
キャンセルを要求するには、Cancel()を呼びます。
C#cts.Cancel();
重要なのは、キャンセルは「処理を外部から強制終了する」のではなく、「処理にキャンセルしてほしいと通知する」仕組みだという点です。
そのため、長いループ処理では定期的に確認します。
C#for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await DoOneStepAsync(cancellationToken);
}
6-5. タイムアウト付きの非同期処理を書く
非同期処理にタイムアウトを付けたい場合は、CancellationTokenSourceのCancelAfterを使う方法があります。
C#using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
try
{
await DownloadAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトまたはキャンセルされました");
}
HTTPリクエストなどでは、ライブラリ側がCancellationTokenを受け取れることが多いです。
C#using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
HttpResponseMessage response =
await httpClient.GetAsync("https://example.com", cts.Token);
また、Task.WhenAnyを使ってタイムアウトを表現する方法もあります。
C#Task<string> workTask = GetDataAsync();
Task timeoutTask = Task.Delay(3000);
Task completed = await Task.WhenAny(workTask, timeoutTask);
if (completed == timeoutTask)
{
Console.WriteLine("タイムアウトしました");
}
else
{
string result = await workTask;
Console.WriteLine(result);
}
ただし、この方法ではタイムアウト後も元の処理が裏で続く可能性があります。処理を本当に止めたい場合は、CancellationTokenと組み合わせるのが基本です。
6-6. 安全にリトライ処理を実装する考え方
通信処理では、一時的なネットワークエラーやサーバーの混雑によって失敗することがあります。このような一時的な失敗には、リトライが有効な場合があります。
単純なリトライの例は次のとおりです。
C#public async Task<string> GetWithRetryAsync(
Func<Task<string>> action,
int maxRetryCount,
CancellationToken cancellationToken)
{
for (int attempt = 1; attempt <= maxRetryCount; attempt++)
{
try
{
return await action();
}
catch when (attempt < maxRetryCount)
{
await Task.Delay(1000, cancellationToken);
}
}
throw new InvalidOperationException("ここには到達しない想定です");
}
ただし、実務では次の点に注意が必要です。
リトライしてよい処理かどうかを確認することが重要です。たとえば、同じ注文登録APIを何度も呼ぶと、注文が重複する危険があります。一方、単なるデータ取得APIならリトライしやすい場合があります。
また、リトライ間隔を固定にすると、障害時に大量のクライアントが同時に再試行し、さらに負荷を高めることがあります。実務では、リトライ間隔を徐々に伸ばす指数バックオフなども検討します。
キャンセルにも対応しておくと、ユーザーが操作を中止したときやアプリ終了時に安全に処理を止められます。
7. async/awaitのベストプラクティス
7-1. asyncは呼び出し元まで伝播させる
async/awaitを使うときは、非同期処理を呼び出す側もasyncにして、呼び出し元まで伝播させるのが基本です。
C#public async Task ControllerActionAsync()
{
var data = await ServiceMethodAsync();
}
途中で.Resultや.Wait()を使って同期的に待とうとすると、デッドロックやスレッドの無駄遣いにつながることがあります。
悪い例です。
C#var data = ServiceMethodAsync().Result;
よい例です。
C#var data = await ServiceMethodAsync();
「非同期で始めたら、最後まで非同期でつなぐ」と考えると分かりやすいです。
7-2. ブロッキング処理と非同期処理を混ぜない
非同期コードの中で、ブロッキングする処理を混ぜると、せっかくの非同期処理のメリットが失われます。
たとえば、次のようなコードは避けるべきです。
C#Thread.Sleep(1000);
非同期メソッド内で待機したい場合は、Task.Delayを使います。
C#await Task.Delay(1000);
また、非同期メソッドの結果を.Resultで取り出すのも避けましょう。
C#var result = GetDataAsync().Result;
代わりにawaitを使います。
C#var result = await GetDataAsync();
非同期処理の中に同期的な待機を混ぜると、パフォーマンス低下やデッドロックの原因になります。
7-3. メソッド名にはAsyncを付ける
C#では、非同期メソッドの名前の末尾にAsyncを付けるのが一般的です。
C#public async Task SaveAsync()
{
await Task.Delay(1000);
}
戻り値がTaskやTask<T>であれば非同期メソッドだと分かりますが、メソッド名にもAsyncを付けることで、呼び出し側がより直感的に理解できます。
C#await SaveAsync();
一方で、次のような名前だと、非同期メソッドかどうかが分かりにくくなります。
C#await Save();
チーム開発では、命名ルールの一貫性が可読性に大きく影響します。自作の非同期メソッドには、原則としてAsyncを付けましょう。
7-4. 不要なTask.Runを使わない
Task.Runは便利ですが、不要な場面で使うと逆効果になることがあります。
特に、すでに非同期APIが用意されている処理をTask.Runで包む必要はありません。
悪い例です。
C#var result = await Task.Run(() => httpClient.GetStringAsync(url));
この場合、GetStringAsync自体が非同期メソッドなので、次のように直接awaitすれば十分です。
C#var result = await httpClient.GetStringAsync(url);
Task.Runは、主にCPU負荷の高い同期処理を別スレッドで実行したい場合に使います。
C#var result = await Task.Run(() => HeavyCalculation());
特にサーバーサイドでは、安易なTask.Runがスレッドプールの消費を増やし、かえってスケーラビリティを下げることがあります。
7-5. ライブラリコードとアプリコードで使い方を分ける
async/awaitの使い方は、ライブラリコードとアプリケーションコードで少し考え方が異なります。
アプリケーションコードでは、UI更新やリクエスト処理など、そのアプリ固有の文脈を意識する必要があります。UIアプリではawait後にUIスレッドへ戻ることが重要な場面があります。
C#var data = await LoadDataAsync();
label.Text = data;
一方、ライブラリコードでは、特定のUIや同期コンテキストに依存しない方が望ましいことがあります。そのため、ライブラリ内部ではConfigureAwait(false)を使う設計もあります。
C#public async Task<string> ReadAsync()
{
return await File.ReadAllTextAsync("sample.txt")
.ConfigureAwait(false);
}
初心者のうちは、まずアプリケーションコードで自然にawaitを書くことを優先しましょう。ライブラリを作る段階になったら、呼び出し元のコンテキストに依存しない設計を意識するとよいです。
7-6. パフォーマンスより可読性と安全性を優先する
async/awaitには、ValueTaskやConfigureAwait(false)、細かな最適化など、パフォーマンスに関する高度な話題があります。
しかし、初心者の段階では、まず可読性と安全性を優先するべきです。
分かりにくい最適化を入れるよりも、次の基本を守る方が重要です。
非同期メソッドは
TaskまたはTask<T>を返す呼び出し側では
awaitする.Resultや.Wait()を避ける例外は
try-catchで扱うキャンセルが必要な処理では
CancellationTokenを使う不要な
Task.Runを使わない
まずは、読みやすく、失敗時の挙動が分かりやすいコードを書くことが大切です。パフォーマンス最適化は、実際に問題が見つかってから検討しても遅くありません。
8. よくある疑問で理解を深める
8-1. asyncを付けるだけで非同期になるのか
asyncを付けるだけでは、処理は非同期になりません。
C#public async Task SampleAsync()
{
Console.WriteLine("Hello");
}
このメソッドにはasyncが付いていますが、awaitがありません。そのため、実質的には同期的に実行されます。
非同期らしい動きになるには、メソッド内で非同期処理をawaitする必要があります。
C#public async Task SampleAsync()
{
await Task.Delay(1000);
Console.WriteLine("Hello");
}
asyncは「このメソッド内でawaitを使えます」という宣言であり、それだけで別スレッド化や遅延実行が起きるわけではありません。
8-2. awaitしないと非同期処理は実行されないのか
非同期メソッドは、呼び出した時点で実行が開始されることが多いです。awaitしないと実行されない、というわけではありません。
C#Task task = DoWorkAsync();
この時点で、DoWorkAsyncの処理は開始されています。ただし、awaitしないと完了を待てません。また、例外も受け取りにくくなります。
C#await task;
awaitは「実行を開始するためのもの」というより、「完了を待ち、結果や例外を受け取るためのもの」と考えると分かりやすいです。
8-3. Task.Runとasync/awaitは何が違うのか
Task.Runとasync/awaitは役割が違います。
Task.Runは、処理をスレッドプールで実行するためのメソッドです。
C#var result = await Task.Run(() => HeavyCalculation());
一方、async/awaitは、非同期処理を読みやすく書くための構文です。
C#var data = await GetDataAsync();
I/O待ちでは、基本的に非同期APIをそのままawaitします。
C#var json = await httpClient.GetStringAsync(url);
CPU負荷の高い処理をUIスレッドから逃がしたい場合などに、Task.Runを検討します。
つまり、Task.Runは「どこで実行するか」に関係し、async/awaitは「非同期処理の完了をどう待つか」に関係します。
8-4. ThreadとTaskは何が違うのか
Threadは、OSレベルのスレッドを直接扱うための仕組みです。低レベルで細かく制御できますが、その分扱いが難しくなります。
C#var thread = new Thread(() =>
{
Console.WriteLine("別スレッドで実行");
});
thread.Start();
一方、Taskは、処理の単位や非同期処理の結果を表す高レベルな仕組みです。
C#Task task = Task.Run(() =>
{
Console.WriteLine("Taskで実行");
});
Taskはスレッドそのものではありません。非同期処理の結果、状態、例外、キャンセルなどを表すオブジェクトです。
現代のC#では、通常のアプリケーション開発で直接Threadを使う場面は少なく、多くの場合はTaskやasync/awaitを使います。
8-5. ValueTaskは初心者でも使うべきか
ValueTaskは、Taskに似た非同期処理の戻り値型です。特定の条件では、不要な割り当てを減らしてパフォーマンスを改善できる場合があります。
C#public ValueTask<int> GetValueAsync()
{
return new ValueTask<int>(10);
}
ただし、ValueTaskは扱いに注意が必要で、初心者が最初から使う必要はほとんどありません。
通常の非同期メソッドでは、まずTaskまたはTask<T>を使えば十分です。
C#public async Task<int> GetValueAsync()
{
await Task.Delay(100);
return 10;
}
ValueTaskは、パフォーマンスが重要なライブラリや、同期的に完了することが非常に多い処理などで検討されます。基本を学ぶ段階では、Taskを理解することを優先しましょう。
8-6. Mainメソッドでasync/awaitを使う方法
コンソールアプリでは、Mainメソッドをasync Task Mainとして書けます。
C#using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("開始");
await RunAsync();
Console.WriteLine("終了");
}
static async Task RunAsync()
{
await Task.Delay(1000);
Console.WriteLine("非同期処理が完了しました");
}
}
戻り値が必要な場合は、Task<int>を返すこともできます。
C#static async Task<int> Main(string[] args)
{
await Task.Delay(1000);
return 0;
}
これにより、コンソールアプリでも.Wait()や.Resultを使わず、自然にawaitを書けます。
まとめ
C#のasync/awaitは、非同期処理を分かりやすく安全に書くための重要な機能です。特にWeb API呼び出し、ファイル操作、データベースアクセス、UIアプリのイベント処理など、待ち時間が発生する処理でよく使われます。
asyncはメソッド内でawaitを使えるようにするキーワードで、awaitは非同期処理の完了を待ち、結果や例外を受け取るためのキーワードです。非同期処理そのものはTaskやTask<T>で表されます。
基本として、戻り値のない非同期メソッドはTask、戻り値のある非同期メソッドはTask<T>を返します。async voidはイベントハンドラーなどの例外的な場面を除き、避けるべきです。
また、.Resultや.Wait()で非同期処理を同期的に待つと、デッドロックやパフォーマンス低下の原因になることがあります。非同期処理は、できるだけ呼び出し元までasync/awaitを伝播させるのが基本です。
Task.WhenAllを使えば複数の非同期処理をまとめて待てます。Task.WhenAnyを使えば最初に完了した処理を扱えます。キャンセルが必要な場合はCancellationTokenを使い、エラー処理はawaitを含む範囲にtry-catchを書くことで自然に扱えます。
最初は難しく感じるかもしれませんが、押さえるべきポイントはシンプルです。
非同期メソッドはTaskを返す、呼び出し側ではawaitする、ブロッキング処理を混ぜない、例外とキャンセルをきちんと扱う。この基本を守れば、C#のasync/awaitを使った読みやすく安全な非同期処理を書けるようになります。

