C#のTaskとは?async/awaitの使い方・非同期処理・よくあるエラーを初心者向けに徹底解説

はじめに

C#で非同期処理を学び始めると、必ず出てくるのがTaskasyncawaitです。名前だけ見ると難しそうですが、Taskは「時間のかかる処理を待つための仕組み」と考えると理解しやすくなります。

たとえば、ファイルの読み込み、Web APIへのアクセス、データベース処理、画像変換、重い計算などは、完了までに時間がかかることがあります。これらを同期的に実行すると、処理が終わるまで画面が固まったり、Webサーバーの応答が遅くなったりします。

そこで使うのがC#のTaskです。Taskを使うと、処理の完了を待ちながらも、スレッドを無駄にブロックしにくいコードを書けます。さらにasync/awaitを組み合わせることで、非同期処理を通常の順番どおりのコードに近い形で読み書きできます。Microsoftの公式ドキュメントでも、C#の非同期プログラミングはasyncawaitTaskを中心に説明されています。Microsoft Learn+1

この記事では、C#のTaskとは何か、async/awaitの基本、Task.Runの使い方、よくあるエラー、実践的なサンプルコードまで、初心者向けに順番に解説します。

1. C#のTaskとは?初心者向けにわかりやすく解説

C#のTaskは、非同期処理を理解するうえで中心になる型です。まずは「Taskは何を表しているのか」を押さえましょう。

1-1. Taskは「非同期処理の結果」を表すオブジェクト

Taskは、簡単にいうと「まだ終わっていないかもしれない処理」を表すオブジェクトです。

たとえば、次のような処理があるとします。

C#
await DownloadFileAsync();

このDownloadFileAsyncメソッドは、内部でファイルのダウンロードを行うかもしれません。ダウンロードには時間がかかるため、すぐに結果が返るとは限りません。

このような「あとで完了する処理」を表すためにTaskを使います。

戻り値がない非同期処理ならTask、戻り値がある非同期処理ならTask<T>を使います。公式ドキュメントでも、値を返さない非同期メソッドにはTask、値を返す非同期メソッドにはTask<TResult>を使うと説明されています。Microsoft Learn+1

C#
public async Task SaveAsync()
{
await Task.Delay(1000);
}

public async Task<string> GetNameAsync()
{
await Task.Delay(1000);
return "田中";
}

SaveAsyncは値を返さないのでTaskGetNameAsyncstringを返すのでTask<string>です。

1-2. Taskを使うと何が便利になるのか

Taskを使うメリットは、時間のかかる処理を効率よく待てることです。

同期処理では、処理が終わるまで現在のスレッドが止まります。一方、非同期処理では、待ち時間の間にスレッドをほかの処理へ回せる場合があります。

たとえば、Web APIからデータを取得する処理を考えてみます。

C#
public async Task<string> GetDataAsync()
{
using HttpClient client = new HttpClient();
string result = await client.GetStringAsync("https://example.com");
return result;
}

このコードでは、HTTP通信が終わるまでawaitで待ちます。しかし、awaitは単純にスレッドを止め続けるわけではありません。非同期処理が完了したら、その続きの処理を再開する仕組みです。

そのため、C#のTaskを正しく使うと、次のようなメリットがあります。

  • UIアプリで画面が固まりにくくなる

  • Webアプリでリクエスト処理の効率が上がる

  • 複数の非同期処理をまとめて扱いやすくなる

  • 例外処理やキャンセル処理を統一的に書ける

  • コードを読みやすく保ちながら非同期処理を書ける

1-3. 同期処理と非同期処理の違い

同期処理は、上から順番に処理が終わるまで待つ書き方です。

C#
Console.WriteLine("開始");
Thread.Sleep(3000);
Console.WriteLine("終了");

このコードでは、Thread.Sleep(3000)の間、現在のスレッドは完全に止まります。

一方、非同期処理では次のように書けます。

C#
Console.WriteLine("開始");
await Task.Delay(3000);
Console.WriteLine("終了");

Task.Delayは、指定した時間が経過したあとに完了するTaskを返します。Thread.Sleepのようにスレッドを占有して眠らせるのではなく、非同期的な待機を表現できます。Task.Delayはタスクベースの非同期処理でよく使われる基本メソッドの一つです。Microsoft Learn

初心者のうちは、次のように考えるとよいでしょう。

同期処理は「終わるまでその場で待つ」処理です。非同期処理は「終わったら続きを実行する」処理です。

1-4. Task・Thread・async/awaitの関係

TaskThreadasync/awaitは混同されやすいですが、それぞれ役割が違います。

Threadは、実際にコードを実行する単位です。一方、Taskは「処理そのもの」や「処理の完了」を表す抽象的なオブジェクトです。

async/awaitは、Taskを扱いやすくするためのC#の構文です。

C#
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("完了");
}

このコードでは、DoWorkAsyncTaskを返し、awaitで非同期処理の完了を待っています。asyncはメソッド内でawaitを使えるようにするキーワードです。

つまり、関係を整理すると次のようになります。

用語役割
Thread実際に処理を実行する流れ
Task非同期処理やその結果を表すオブジェクト
async非同期メソッドであることを示すキーワード
awaitTaskの完了を非同期的に待つキーワード

1-5. Taskがよく使われる場面

C#のTaskは、さまざまな場面で使われます。

代表的なのは、Web API通信です。

C#
string json = await httpClient.GetStringAsync(url);

ファイル読み込みでも使われます。

C#
string text = await File.ReadAllTextAsync("sample.txt");

データベースアクセスでも、Entity Framework Coreなどでは非同期メソッドがよく使われます。

C#
var users = await dbContext.Users.ToListAsync();

また、重い計算処理をUIスレッドから切り離したい場合にはTask.Runを使うこともあります。

C#
int result = await Task.Run(() => HeavyCalculation());

ただし、Task.Runは何でも速くする魔法ではありません。使いどころを間違えると、かえって効率が悪くなることがあります。この点は後半で詳しく解説します。

2. C#の非同期処理の基本

Taskを正しく使うには、まず非同期処理の考え方を理解する必要があります。

2-1. 非同期処理とは何か

非同期処理とは、「時間のかかる処理の完了を待っている間、現在の処理を無駄に止め続けない仕組み」です。

たとえば、Web APIにアクセスする場合、プログラムはネットワークの応答を待つ必要があります。この待ち時間中、CPUは実際にはほとんど仕事をしていません。

同期処理では、その待ち時間の間もスレッドがブロックされます。非同期処理では、待っている間にスレッドを解放し、完了後に続きの処理を実行できます。

C#では、このような非同期処理をTaskasync/awaitで自然に書けます。

C#
public async Task LoadAsync()
{
Console.WriteLine("読み込み開始");

await Task.Delay(1000);

Console.WriteLine("読み込み完了");
}

2-2. UIアプリやWebアプリで非同期処理が重要な理由

UIアプリでは、画面を描画したりボタン操作を受け付けたりするためのUIスレッドがあります。このUIスレッドで時間のかかる同期処理を実行すると、画面が固まります。

悪い例です。

C#
private void Button_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
label.Text = "完了";
}

この場合、5秒間画面が反応しなくなります。

非同期処理を使うと、画面の応答性を保ちやすくなります。

C#
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(5000);
label.Text = "完了";
}

Webアプリでも非同期処理は重要です。データベースや外部APIの応答を待つ間にスレッドをブロックすると、同時に処理できるリクエスト数が減る可能性があります。ASP.NET Coreなどでは、I/O処理に非同期APIを使うことが一般的です。

2-3. I/Oバウンド処理とCPUバウンド処理の違い

非同期処理を理解するには、I/Oバウンド処理とCPUバウンド処理の違いが重要です。

I/Oバウンド処理とは、ネットワーク、ファイル、データベースなど、外部の応答待ちが中心の処理です。

例は次のとおりです。

C#
await httpClient.GetStringAsync(url);
await File.ReadAllTextAsync(path);
await dbContext.Users.ToListAsync();

CPUバウンド処理とは、CPUで計算する時間が中心の処理です。

例は次のとおりです。

C#
int result = CalculatePrimeNumbers();

I/Oバウンド処理では、基本的に非同期APIをそのままawaitするのが適切です。CPUバウンド処理をUIスレッドから逃がしたい場合には、Task.Runを使うことがあります。

2-4. 「非同期」と「並列処理」は同じではない

初心者がよく混乱するポイントとして、「非同期」と「並列処理」の違いがあります。

非同期処理は、待ち時間を効率よく扱うための仕組みです。並列処理は、複数の処理を同時に実行して処理時間を短縮するための仕組みです。

たとえば、次のコードは非同期的に順番に実行しています。

C#
await Task.Delay(1000);
await Task.Delay(1000);
await Task.Delay(1000);

これは合計で約3秒かかります。

一方、次のコードは複数のTaskを同時に開始し、まとめて待っています。

C#
Task task1 = Task.Delay(1000);
Task task2 = Task.Delay(1000);
Task task3 = Task.Delay(1000);

await Task.WhenAll(task1, task2, task3);

これは約1秒で完了します。

Task.WhenAllは、複数のTaskがすべて完了するのを非同期的に待つためのメソッドです。Microsoft Learn+1

2-5. Taskを使う前に理解しておきたい実行の流れ

awaitを使ったコードは、見た目は上から順番に実行されます。

C#
Console.WriteLine("A");

await Task.Delay(1000);

Console.WriteLine("B");

実行結果は次のようになります。

A
B

ただし、await Task.Delay(1000)の部分で、メソッドはいったん中断されます。Task.Delayが完了すると、中断した場所から再開します。

awaitは「ここで処理を完全に止める」というより、「このTaskが終わったら続きを実行する」と考えると理解しやすいです。

3. Taskの基本的な使い方

ここからは、C#でTaskを使う基本パターンを見ていきます。

3-1. Taskを返すメソッドの書き方

値を返さない非同期メソッドは、戻り値の型をTaskにします。

C#
public async Task SaveDataAsync()
{
await Task.Delay(1000);
Console.WriteLine("保存しました");
}

呼び出す側ではawaitします。

C#
await SaveDataAsync();

メソッド名には、非同期メソッドであることがわかるようにAsyncを付けるのが一般的です。

3-2. Task<T>で戻り値を受け取る方法

値を返す非同期メソッドでは、Task<T>を使います。

C#
public async Task<int> GetCountAsync()
{
await Task.Delay(1000);
return 10;
}

呼び出す側では、awaitするとTの値を受け取れます。

C#
int count = await GetCountAsync();
Console.WriteLine(count);

Task<int>は「将来的にintを返す処理」を表しています。awaitすると、完了後のint値を取り出せます。

3-3. Task.CompletedTaskの使い方

すでに完了しているTaskを返したい場合は、Task.CompletedTaskを使えます。

C#
public Task LogAsync(string message)
{
Console.WriteLine(message);
return Task.CompletedTask;
}

このようなコードは、インターフェースの都合でTaskを返す必要があるけれど、実際には非同期処理がない場合に使われます。

C#
public interface ILogger
{
Task LogAsync(string message);
}

実装側で今は同期処理しかしていない場合でも、Task.CompletedTaskを返せばTask型の契約を満たせます。

3-4. Task.FromResultの使い方

値を持つ完了済みのTask<T>を返したい場合は、Task.FromResultを使います。

C#
public Task<int> GetNumberAsync()
{
return Task.FromResult(100);
}

これは、すぐに100を返せるけれど、メソッドの戻り値としてはTask<int>が必要な場合に便利です。

キャッシュ済みの値を返す場合にもよく使われます。

C#
public Task<string> GetUserNameAsync(int userId)
{
if (userId == 1)
{
return Task.FromResult("山田");
}

return Task.FromResult("不明");
}

3-5. Task.Delayで待機処理を書く方法

Task.Delayは、指定した時間だけ非同期的に待機するメソッドです。

C#
public async Task WaitAsync()
{
Console.WriteLine("待機開始");

await Task.Delay(3000);

Console.WriteLine("3秒経過");
}

Thread.Sleepとは違い、Task.Delayは非同期処理での待機に向いています。

悪い例です。

C#
public async Task BadAsync()
{
Thread.Sleep(3000);
}

よい例です。

C#
public async Task GoodAsync()
{
await Task.Delay(3000);
}

非同期メソッド内で時間待ちを表現したい場合は、基本的にTask.Delayを使いましょう。

3-6. Task.Runで別スレッドに処理を渡す方法

Task.Runは、処理をスレッドプール上で実行するためのメソッドです。

C#
public async Task<int> CalculateAsync()
{
int result = await Task.Run(() =>
{
int total = 0;

for (int i = 0; i < 1000000; i++)
{
total += i;
}

return total;
});

return result;
}

Task.Runは、重いCPU処理をUIスレッドから切り離したい場合に役立ちます。

ただし、HTTP通信やファイル読み込みのようなI/O処理に対して、何でもTask.Runで包むのは適切ではありません。I/O処理には、最初から用意されている非同期APIを使うのが基本です。

4. async/awaitの使い方

Taskを扱ううえで欠かせないのがasync/awaitです。

4-1. asyncとは何か

asyncは、そのメソッドが非同期メソッドであることを示すキーワードです。

C#
public async Task DoSomethingAsync()
{
await Task.Delay(1000);
}

asyncを付けると、メソッド内でawaitを使えるようになります。

ただし、asyncを付けただけで自動的に別スレッドで実行されるわけではありません。asyncは、非同期処理を扱うための構文を有効にするものです。

4-2. awaitとは何か

awaitは、Taskの完了を待つためのキーワードです。

C#
await Task.Delay(1000);

awaitは、対象の非同期処理が完了するまで、現在の非同期メソッドの実行をいったん中断します。完了後、続きの処理が再開されます。公式ドキュメントでも、awaitは非同期操作が完了するまで、それを含むasyncメソッドの評価を中断すると説明されています。Microsoft Learn

戻り値があるTask<T>awaitすると、結果の値を受け取れます。

C#
int number = await GetNumberAsync();

4-3. async Taskメソッドの基本構文

値を返さない非同期メソッドは、次のように書きます。

C#
public async Task PrintAsync()
{
await Task.Delay(1000);
Console.WriteLine("完了");
}

呼び出し側は次のようになります。

C#
await PrintAsync();

async Taskは、C#の非同期メソッドで最もよく使う形の一つです。

4-4. async Task<T>で値を返す方法

値を返す場合は、Task<T>を使います。

C#
public async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "こんにちは";
}

呼び出し側です。

C#
string message = await GetMessageAsync();
Console.WriteLine(message);

ここで重要なのは、メソッド内ではstringreturnしているのに、メソッドの戻り値の型はTask<string>であることです。

C#
public async Task<string> GetMessageAsync()
{
return "こんにちは";
}

asyncメソッドでは、戻り値の型がTask<T>の場合、returnにはT型の値を書きます。

4-5. awaitしない場合の挙動

Taskを返すメソッドを呼んでも、awaitしなければ完了を待ちません。

C#
DoSomethingAsync();
Console.WriteLine("次の処理");

この場合、DoSomethingAsyncの完了を待たずにConsole.WriteLineが実行される可能性があります。

また、コンパイラから次のような警告が出ることがあります。

Because this call is not awaited, execution of the current method continues before the call is completed.

つまり、「awaitされていないので、処理が完了する前に次へ進みます」という警告です。

意図的に待たない場合もありますが、初心者のうちは基本的にawaitを付けると覚えておきましょう。

C#
await DoSomethingAsync();

4-6. async voidを避けるべき理由

非同期メソッドでは、基本的にasync voidを避けます。

悪い例です。

C#
public async void SaveAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}

async voidは呼び出し側でawaitできません。また、例外を呼び出し側で捕捉しにくくなります。

よい例です。

C#
public async Task SaveAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}

async voidは、主にイベントハンドラで使います。公式ドキュメントでも、非同期メソッドの戻り値としてvoidは主にイベントハンドラ向けとされています。Microsoft Learn+1

C#
private async void Button_Click(object sender, EventArgs e)
{
await SaveAsync();
}

通常のメソッドでは、async Taskまたはasync Task<T>を使いましょう。

4-7. 実践コード:async/awaitで非同期処理を書く

以下は、APIから文字列を取得するイメージのサンプルです。

C#
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class ApiClient
{
private readonly HttpClient _httpClient = new HttpClient();

public async Task<string> GetHtmlAsync(string url)
{
string html = await _httpClient.GetStringAsync(url);
return html;
}
}

public class Program
{
public static async Task Main()
{
var client = new ApiClient();

string html = await client.GetHtmlAsync("https://example.com");

Console.WriteLine(html);
}
}

このコードでは、GetHtmlAsyncTask<string>を返し、Mainメソッド側でawaitして結果を受け取っています。

5. Taskを使った実践パターン

Taskは単体で使うだけでなく、複数の非同期処理を組み合わせる場面でもよく使われます。

5-1. 1つの非同期処理を順番に実行する

最も基本的なパターンは、1つずつ順番にawaitする方法です。

C#
public async Task ExecuteAsync()
{
await Step1Async();
await Step2Async();
await Step3Async();
}

private async Task Step1Async()
{
await Task.Delay(1000);
Console.WriteLine("Step1");
}

private async Task Step2Async()
{
await Task.Delay(1000);
Console.WriteLine("Step2");
}

private async Task Step3Async()
{
await Task.Delay(1000);
Console.WriteLine("Step3");
}

この場合、Step1Asyncが終わってからStep2Async、その後にStep3Asyncが実行されます。

処理の順番に意味がある場合は、この書き方が適しています。

5-2. 複数のTaskを同時に実行する

処理同士に依存関係がない場合は、複数のTaskを同時に開始できます。

C#
public async Task ExecuteAsync()
{
Task task1 = Task.Delay(1000);
Task task2 = Task.Delay(1000);
Task task3 = Task.Delay(1000);

await task1;
await task2;
await task3;

Console.WriteLine("すべて完了");
}

このコードでは、3つのTask.Delayがほぼ同時に開始されます。そのため、合計時間は約3秒ではなく約1秒になります。

ただし、複数のTaskを待つ場合は、次に紹介するTask.WhenAllの方がよく使われます。

5-3. Task.WhenAllで複数処理の完了を待つ

Task.WhenAllは、複数のTaskがすべて完了するまで待つメソッドです。

C#
public async Task ExecuteAsync()
{
Task task1 = DownloadAsync("A");
Task task2 = DownloadAsync("B");
Task task3 = DownloadAsync("C");

await Task.WhenAll(task1, task2, task3);

Console.WriteLine("すべてのダウンロードが完了しました");
}

private async Task DownloadAsync(string name)
{
await Task.Delay(1000);
Console.WriteLine($"{name}をダウンロードしました");
}

戻り値がある場合は、次のように書けます。

C#
public async Task ExecuteAsync()
{
Task<int> task1 = GetNumberAsync(1);
Task<int> task2 = GetNumberAsync(2);
Task<int> task3 = GetNumberAsync(3);

int[] results = await Task.WhenAll(task1, task2, task3);

Console.WriteLine(results.Sum());
}

private async Task<int> GetNumberAsync(int number)
{
await Task.Delay(1000);
return number;
}

Task.WhenAllは、複数のAPI呼び出しや複数ファイルの読み込みなどでよく使います。

5-4. Task.WhenAnyで最初に終わった処理を受け取る

Task.WhenAnyは、複数のTaskのうち最初に完了したものを受け取るメソッドです。

C#
public async Task ExecuteAsync()
{
Task<string> task1 = GetDataAsync("A", 3000);
Task<string> task2 = GetDataAsync("B", 1000);
Task<string> task3 = GetDataAsync("C", 2000);

Task<string> completedTask = await Task.WhenAny(task1, task2, task3);

string result = await completedTask;

Console.WriteLine($"最初に完了した結果: {result}");
}

private async Task<string> GetDataAsync(string name, int delay)
{
await Task.Delay(delay);
return name;
}

この例では、最も早く完了するBの結果が表示されます。

Task.WhenAnyは、タイムアウト処理や、複数の候補から最初に応答したものを使いたい場合に便利です。Microsoft Learn+1

5-5. CancellationTokenでTaskをキャンセルする

CancellationTokenを使うと、非同期処理にキャンセル要求を伝えられます。

C#
public async Task WorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"{i}回目の処理");
await Task.Delay(1000, cancellationToken);
}
}

呼び出し側です。

C#
using var cts = new CancellationTokenSource();

Task task = WorkAsync(cts.Token);

cts.Cancel();

try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}

CancellationTokenは、処理を強制終了するものではありません。キャンセル要求を通知し、処理側がそれに協調して応答する仕組みです。公式ドキュメントでも、CancellationTokenTaskなどの間で協調的なキャンセルを可能にするものと説明されています。Microsoft Learn+1

5-6. タイムアウト付きの非同期処理を書く

タイムアウトを実装する方法はいくつかあります。シンプルな方法として、CancellationTokenSource.CancelAfterを使えます。

C#
public async Task ExecuteWithTimeoutAsync()
{
using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(3));

try
{
await LongRunningAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトしました");
}
}

private async Task LongRunningAsync(CancellationToken cancellationToken)
{
await Task.Delay(10000, cancellationToken);
}

このコードでは、3秒後にキャンセル要求が発行されます。

Task.DelayCancellationTokenを渡しているため、キャンセルされるとOperationCanceledExceptionが発生します。

5-7. 例外処理をtry-catchで扱う

awaitしているTask内で例外が発生した場合、通常のtry-catchで捕捉できます。

C#
public async Task ExecuteAsync()
{
try
{
await ThrowErrorAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}

private async Task ThrowErrorAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("不正な操作です");
}

async/awaitを使うと、非同期処理でも同期処理に近い感覚で例外処理を書けます。

ただし、awaitせずに放置したTaskの例外は扱いにくくなります。基本的には、非同期処理を呼び出したらawaitしましょう。

6. Task.Runの正しい使い方と注意点

Task.Runは便利ですが、誤解されやすいメソッドです。

6-1. Task.Runは何をするメソッドか

Task.Runは、指定した処理をスレッドプール上で実行するメソッドです。

C#
Task task = Task.Run(() =>
{
Console.WriteLine("別スレッドで実行");
});

戻り値がある場合は、Task<T>になります。

C#
Task<int> task = Task.Run(() =>
{
return 1 + 2;
});

int result = await task;

Task.Runを使うと、重い処理を現在のスレッドから切り離せます。

6-2. Task.Runが向いている処理

Task.Runが向いているのは、CPUを使う重い処理です。

たとえば、次のような処理です。

  • 大量データの集計

  • 画像処理

  • 暗号化やハッシュ計算

  • 複雑な数値計算

  • UIスレッドで実行すると画面が固まる処理

サンプルです。

C#
public async Task<int> CalculateAsync()
{
int result = await Task.Run(() =>
{
int total = 0;

for (int i = 0; i < 100000000; i++)
{
total += i % 10;
}

return total;
});

return result;
}

UIアプリでは、このようにTask.Runを使うことで、画面のフリーズを防ぎやすくなります。

6-3. Task.Runが不要な処理

HTTP通信やファイル読み込みなど、すでに非同期APIがある処理では、基本的にTask.Runは不要です。

不要な例です。

C#
string result = await Task.Run(() =>
{
return httpClient.GetStringAsync(url).Result;
});

よい例です。

C#
string result = await httpClient.GetStringAsync(url);

GetStringAsync自体が非同期メソッドなので、そのままawaitすれば十分です。

ファイル読み込みも同じです。

C#
string text = await File.ReadAllTextAsync("sample.txt");

非同期APIがある場合は、それを直接使うのが基本です。

6-4. WebアプリでTask.Runを乱用してはいけない理由

ASP.NET CoreなどのWebアプリでTask.Runを乱用すると、スレッドプールのスレッドを余計に消費する可能性があります。

Webアプリでは、リクエストを処理するために多くのスレッドが必要です。I/O待ちの処理をTask.Runで包むと、本来不要なスレッドを使ってしまうことがあります。

悪い例です。

C#
public async Task<IActionResult> Index()
{
string data = await Task.Run(() =>
{
return _service.GetDataAsync().Result;
});

return Ok(data);
}

よい例です。

C#
public async Task<IActionResult> Index()
{
string data = await _service.GetDataAsync();
return Ok(data);
}

Webアプリでは、I/O処理には非同期APIを使い、Task.Runで無理に別スレッドへ逃がさないようにしましょう。

6-5. 重いCPU処理をTask.Runで実行する例

次は、重い計算処理をTask.Runで実行する例です。

C#
public async Task<int> CountPrimesAsync(int max)
{
return await Task.Run(() =>
{
int count = 0;

for (int i = 2; i <= max; i++)
{
if (IsPrime(i))
{
count++;
}
}

return count;
});
}

private bool IsPrime(int number)
{
if (number < 2)
{
return false;
}

for (int i = 2; i * i <= number; i++)
{
if (number % i == 0)
{
return false;
}
}

return true;
}

このようなCPUバウンド処理は、UIアプリではTask.Runの候補になります。

6-6. asyncメソッド内でTask.Runを使うときの注意点

asyncメソッド内でTask.Runを使うこと自体は可能です。

C#
public async Task ExecuteAsync()
{
int result = await Task.Run(() => HeavyCalculation());
Console.WriteLine(result);
}

ただし、次のように非同期メソッドをTask.Runで包む必要は基本的にありません。

C#
await Task.Run(async () =>
{
await SomeIoAsync();
});

I/O処理であれば、次のように直接awaitしましょう。

C#
await SomeIoAsync();

Task.Runは「非同期にするための万能メソッド」ではありません。CPUバウンド処理を別スレッドに渡したいときに使う、と覚えておくと失敗しにくくなります。

7. Taskでよくあるエラーと解決方法

ここでは、C#のTaskasync/awaitで初心者がつまずきやすいエラーを解説します。

7-1. awaitを付け忘れて処理結果が取得できない

よくあるミスが、awaitの付け忘れです。

悪い例です。

C#
Task<string> task = GetNameAsync();
Console.WriteLine(task);

この場合、表示されるのはstringの結果ではなく、Task<string>オブジェクトの情報です。

よい例です。

C#
string name = await GetNameAsync();
Console.WriteLine(name);

Task<T>からTの値を取り出すには、基本的にawaitが必要です。

7-2. async voidにして例外を捕捉できない

async voidは呼び出し側でawaitできないため、例外処理が難しくなります。

悪い例です。

C#
public async void DoAsync()
{
await Task.Delay(1000);
throw new Exception("失敗しました");
}

よい例です。

C#
public async Task DoAsync()
{
await Task.Delay(1000);
throw new Exception("失敗しました");
}

呼び出し側では次のように例外を捕捉できます。

C#
try
{
await DoAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

async voidはイベントハンドラ以外では避けましょう。

7-3. .Resultや.Wait()でデッドロックする

Taskの結果を同期的に取り出すために.Result.Wait()を使うと、デッドロックやスレッドブロックの原因になることがあります。

悪い例です。

C#
string result = GetDataAsync().Result;
C#
GetDataAsync().Wait();

よい例です。

C#
string result = await GetDataAsync();

特にUIアプリや古いASP.NETでは、.Result.Wait()がデッドロックを引き起こす代表的な原因になります。非同期処理は、できるだけ最後までawaitでつなげるのが安全です。

7-4. 戻り値の型をTaskにすべきかTask<T>にすべきか迷う

戻り値の型は、次の基準で選びます。

処理内容戻り値の型
非同期で値を返さないTask
非同期で値を返すTask<T>
イベントハンドラasync void
同期的に値を返すT
同期的に何も返さないvoid

例です。

C#
public async Task SaveAsync()
{
await Task.Delay(1000);
}
C#
public async Task<int> GetCountAsync()
{
await Task.Delay(1000);
return 10;
}

「値を返すならTask<T>、返さないならTask」と覚えておけば、多くの場面で判断できます。

7-5. Task.Runを書いたのに処理が速くならない

Task.Runを書いても、必ず処理が速くなるわけではありません。

たとえば、次のような処理は意味が薄いです。

C#
await Task.Run(() => Thread.Sleep(1000));

これは別スレッドを1秒間ブロックしているだけです。

また、1つの重い処理を別スレッドに移しても、処理自体の計算量が減るわけではありません。UIの応答性は改善するかもしれませんが、総処理時間が大きく短くなるとは限りません。

Task.Runは処理を別スレッドで実行する手段であり、処理を自動的に高速化する機能ではありません。

7-6. 例外がAggregateExceptionになる

.Wait().Resultを使うと、例外がAggregateExceptionに包まれることがあります。

C#
try
{
GetDataAsync().Wait();
}
catch (AggregateException ex)
{
Console.WriteLine(ex.InnerException?.Message);
}

一方、awaitを使うと、元の例外をそのまま扱いやすくなります。

C#
try
{
await GetDataAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}

例外処理の面でも、.Wait().Resultよりawaitを使う方がわかりやすいです。

7-7. CancellationTokenが効かない

CancellationTokenを渡しただけでは、処理は自動的に止まりません。

効かない例です。

C#
public async Task WorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(1000);
Console.WriteLine(i);
}
}

このコードでは、tokenを受け取っていますが、使っていません。

よい例です。

C#
public async Task WorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();

await Task.Delay(1000, token);

Console.WriteLine(i);
}
}

CancellationTokenを有効にするには、処理の中でキャンセル要求を確認する必要があります。

7-8. ConfigureAwait(false)はいつ使うべきか

ConfigureAwait(false)は、await後に元の同期コンテキストへ戻る必要がないことを示すために使われます。

C#
string result = await httpClient.GetStringAsync(url).ConfigureAwait(false);

ライブラリコードでは、ConfigureAwait(false)が使われることがあります。UI更新が不要な処理で元のコンテキストに戻らなくてよい場合、効率やデッドロック回避の観点で使われることがあります。

ただし、初心者が最初からすべてのawaitに付ける必要はありません。

UIアプリでConfigureAwait(false)を使ったあとにUIを更新しようとすると、問題になることがあります。

C#
await SomeAsync().ConfigureAwait(false);

// UIスレッドに戻っているとは限らない
label.Text = "完了";

まずはasync/awaitTaskの基本を理解し、ライブラリ開発や高度なチューニングが必要になった段階で学ぶとよいでしょう。

8. TaskとThreadの違い

TaskThreadは似ているように見えますが、考え方が異なります。

8-1. Threadは実行単位、Taskは作業の結果を表す

Threadは、実際にコードを実行する流れです。

C#
Thread thread = new Thread(() =>
{
Console.WriteLine("Threadで実行");
});

thread.Start();

一方、Taskは作業やその完了を表します。

C#
Task task = Task.Run(() =>
{
Console.WriteLine("Taskで実行");
});

Threadはより低レベルな仕組みで、Taskはより高レベルで扱いやすい仕組みです。

8-2. Taskはスレッドを直接管理しない

Taskを使うと、必ず新しいスレッドが作られるわけではありません。

たとえば、Task.Delayは待機を表しますが、その間ずっと専用スレッドが動いているわけではありません。

C#
await Task.Delay(1000);

また、I/Oバウンドの非同期処理では、待機中にスレッドを占有しない場合があります。

Taskは「処理の完了」を表すものであり、「スレッドそのもの」ではありません。

8-3. ThreadPoolとTaskの関係

Task.Runで実行される処理は、通常スレッドプール上で実行されます。

スレッドプールは、あらかじめ用意されたスレッドを再利用する仕組みです。毎回新しいThreadを作るよりも効率的です。

C#
await Task.Run(() =>
{
Console.WriteLine("ThreadPool上で実行される");
});

Taskは、こうしたスレッドプールを利用しながら、開発者が直接スレッドを細かく管理しなくてもよいようにしてくれます。

8-4. 初心者はThreadよりTaskを使うべき理由

初心者が非同期処理を学ぶ場合、基本的にはThreadよりTaskを優先して学ぶのがおすすめです。

理由は、Taskの方が次の点で扱いやすいからです。

  • async/awaitと組み合わせて読みやすく書ける

  • 戻り値をTask<T>で扱える

  • 例外処理を書きやすい

  • キャンセル処理と組み合わせやすい

  • Task.WhenAllTask.WhenAnyで複数処理を扱いやすい

現在のC#で一般的な非同期処理を書くなら、まずはTaskasync/awaitを理解するのが近道です。

8-5. TaskとThreadの使い分け早見表

比較項目TaskThread
抽象度高い低い
主な用途非同期処理、並行処理の管理スレッドを直接制御
戻り値Task<T>で扱いやすい自前で管理が必要
例外処理awaittry-catchで扱いやすい自前で管理が必要
キャンセルCancellationTokenと相性がよい自前で設計が必要
初心者向け向いているやや難しい

基本的には、通常のアプリ開発ではTaskを使い、特別な理由がある場合にThreadを検討するとよいでしょう。

9. C# Taskのベストプラクティス

ここでは、C#でTaskを使うときの実践的なベストプラクティスを紹介します。

9-1. 非同期メソッド名にはAsyncを付ける

非同期メソッドには、名前の末尾にAsyncを付けるのが一般的です。

C#
public Task SaveAsync()
{
return Task.CompletedTask;
}
C#
public Task<User> GetUserAsync(int id)
{
return Task.FromResult(new User());
}

Asyncを付けることで、呼び出し側が非同期メソッドだとすぐに判断できます。

9-2. async voidはイベントハンドラ以外で使わない

通常の非同期メソッドでは、async voidではなくasync Taskを使いましょう。

悪い例です。

C#
public async void LoadAsync()
{
await Task.Delay(1000);
}

よい例です。

C#
public async Task LoadAsync()
{
await Task.Delay(1000);
}

例外処理やテストのしやすさを考えると、Taskを返す方が安全です。

9-3. 可能な限りawaitを使う

Taskを呼び出したら、基本的にはawaitしましょう。

C#
await SaveAsync();

awaitしない場合、その処理の完了タイミングや例外を管理しにくくなります。

どうしても待たない処理を書く場合は、ログ出力や例外処理をどうするかまで設計する必要があります。

9-4. .Resultや.Wait()を避ける

非同期処理を同期的に待つ.Result.Wait()は、できるだけ避けましょう。

悪い例です。

C#
var user = GetUserAsync().Result;

よい例です。

C#
var user = await GetUserAsync();

非同期処理は、呼び出し元までasync/awaitを広げていくのが基本です。

9-5. 例外処理とキャンセル処理を必ず考慮する

非同期処理では、通信失敗、タイムアウト、キャンセルなどが起こります。

C#
try
{
await DownloadAsync(cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}

特に外部APIやファイル、データベースを扱う処理では、正常系だけでなく異常系も考えておきましょう。

9-6. I/O処理ではTask.Runではなく非同期APIを使う

I/O処理では、Task.Runで同期メソッドを包むより、用意されている非同期APIを使うのが基本です。

悪い例です。

C#
string text = await Task.Run(() => File.ReadAllText("sample.txt"));

よい例です。

C#
string text = await File.ReadAllTextAsync("sample.txt");

HTTP通信も同じです。

C#
string json = await httpClient.GetStringAsync(url);

非同期APIがある場合は、それをそのままawaitしましょう。

9-7. 読みやすい非同期コードを書くコツ

非同期コードは、書き方を間違えると複雑になりがちです。読みやすくするためには、次の点を意識しましょう。

  • メソッド名にAsyncを付ける

  • TaskTask<T>を適切に使い分ける

  • awaitを省略しすぎない

  • Task.Runを乱用しない

  • CancellationTokenを必要なメソッドに渡す

  • 例外処理を呼び出し側でまとめる

  • 1つのメソッドに処理を詰め込みすぎない

たとえば、次のように処理を分けると読みやすくなります。

C#
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
string json = await DownloadJsonAsync(cancellationToken);
User user = ParseUser(json);
await SaveUserAsync(user, cancellationToken);
}

非同期処理でも、基本は「小さく、わかりやすく、責務を分ける」ことが大切です。

10. C# Taskのサンプルコード集

ここでは、C#のTaskを学ぶためのサンプルコードをまとめます。

10-1. Taskを使った最小サンプル

C#
using System;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
await SayHelloAsync();
}

private static async Task SayHelloAsync()
{
await Task.Delay(1000);
Console.WriteLine("Hello Task!");
}
}

SayHelloAsyncは1秒待ってからメッセージを表示します。

10-2. async/awaitの基本サンプル

C#
using System;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
Console.WriteLine("開始");

await DoWorkAsync();

Console.WriteLine("終了");
}

private static async Task DoWorkAsync()
{
await Task.Delay(2000);
Console.WriteLine("処理中");
}
}

実行順は次のようになります。

開始
処理中
終了

10-3. Task<T>で戻り値を受け取るサンプル

C#
using System;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
int result = await AddAsync(10, 20);

Console.WriteLine(result);
}

private static async Task<int> AddAsync(int a, int b)
{
await Task.Delay(1000);
return a + b;
}
}

Task<int>awaitすると、intの結果を受け取れます。

10-4. Task.WhenAllで複数APIを同時実行するサンプル

C#
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
using HttpClient client = new HttpClient();

Task<string> task1 = client.GetStringAsync("https://example.com");
Task<string> task2 = client.GetStringAsync("https://example.org");
Task<string> task3 = client.GetStringAsync("https://example.net");

string[] results = await Task.WhenAll(task1, task2, task3);

foreach (string result in results)
{
Console.WriteLine(result.Length);
}
}
}

複数のリクエストに依存関係がない場合、同時に開始してTask.WhenAllで待つと効率的です。

10-5. Task.Runで重い処理を非同期化するサンプル

C#
using System;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
int result = await Task.Run(() => HeavyCalculation());

Console.WriteLine(result);
}

private static int HeavyCalculation()
{
int total = 0;

for (int i = 0; i < 100000000; i++)
{
total += i % 10;
}

return total;
}
}

CPUを使う重い処理を別スレッドで実行したい場合にTask.Runを使えます。

10-6. CancellationTokenでキャンセルするサンプル

C#
using System;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
using var cts = new CancellationTokenSource();

Task task = WorkAsync(cts.Token);

await Task.Delay(2500);

cts.Cancel();

try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました");
}
}

private static async Task WorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"{i}回目の処理");

await Task.Delay(1000, cancellationToken);
}
}
}

CancellationTokenを処理内で確認することで、キャンセル可能なTaskを書けます。Taskのキャンセルでは、キャンセル要求を出す側と処理する側が協調して動く必要があります。Microsoft Learn+1

10-7. try-catchで例外処理するサンプル

C#
using System;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
try
{
await ErrorAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"例外を捕捉しました: {ex.Message}");
}
}

private static async Task ErrorAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("サンプルエラー");
}
}

awaitしていれば、非同期メソッド内の例外も通常のtry-catchで扱えます。

11. C# Taskに関するよくある質問

最後に、C#のTaskについてよくある質問に答えます。

11-1. Taskとasync/awaitは必ずセットで使うのか

必ずセットではありません。

Taskasync/awaitなしでも使えます。

C#
Task task = Task.Run(() => Console.WriteLine("実行"));

また、Task.CompletedTaskTask.FromResultのように、asyncを付けずにTaskを返すこともあります。

C#
public Task<int> GetValueAsync()
{
return Task.FromResult(10);
}

ただし、実際の非同期処理を書く場合は、async/awaitを使った方が読みやすく、例外処理もしやすくなります。

11-2. Taskを使えば必ず処理は速くなるのか

必ず速くなるわけではありません。

Taskは、非同期処理や並行処理を扱いやすくする仕組みです。処理そのものの計算量が減るわけではありません。

I/O待ちが多い処理では、スレッドを効率よく使えるため、アプリ全体の応答性やスケーラビリティが改善することがあります。

一方、CPUバウンド処理では、Task.Runを使っても単体の処理時間が必ず短くなるとは限りません。

11-3. awaitすると処理は止まるのか

awaitすると、その非同期メソッドの続きは対象のTaskが完了するまで待ちます。

ただし、Thread.Sleepのようにスレッドを単純に止め続けるわけではありません。awaitは、非同期処理が完了したら続きを再開する仕組みです。

C#
Console.WriteLine("A");

await Task.Delay(1000);

Console.WriteLine("B");

この場合、Bは1秒後に実行されます。

11-4. Task.Runとasyncメソッドは何が違うのか

Task.Runは、処理をスレッドプールで実行するメソッドです。

C#
await Task.Run(() => HeavyCalculation());

一方、asyncメソッドは、awaitを使って非同期処理を書けるメソッドです。

C#
public async Task LoadAsync()
{
await Task.Delay(1000);
}

asyncを付けたからといって、自動的に別スレッドで実行されるわけではありません。Task.Runは主にCPUバウンド処理を別スレッドへ渡したい場合に使います。

11-5. asyncメソッドでreturnできる型は何か

C#の非同期メソッドでは、主に次の戻り値の型を使います。

戻り値の型用途
Task値を返さない非同期処理
Task<T>値を返す非同期処理
voidイベントハンドラ
IAsyncEnumerable<T>非同期ストリーム

初心者がまず覚えるべきなのは、TaskTask<T>です。voidはイベントハンドラ以外では避けましょう。Microsoft Learn+1

11-6. ConfigureAwait(false)は初心者も覚えるべきか

最初から深く理解する必要はありません。

初心者はまず、次の内容を優先して学びましょう。

  • TaskTask<T>の違い

  • async/awaitの基本

  • Task.WhenAllの使い方

  • Task.Runの使いどころ

  • .Result.Wait()を避ける理由

  • CancellationTokenの基本

ConfigureAwait(false)は、ライブラリ開発やUIスレッド、同期コンテキストについて理解が進んでから学ぶとよいテーマです。

11-7. C#で非同期処理を学ぶ順番はどうすればよいか

C#で非同期処理を学ぶなら、次の順番がおすすめです。

  1. TaskTask<T>の意味を理解する

  2. async/awaitの基本構文を覚える

  3. Task.Delayで簡単な非同期処理を書く

  4. HttpClientやファイル読み込みで非同期APIを使う

  5. Task.WhenAllで複数処理を同時に待つ

  6. try-catchで例外処理を書く

  7. CancellationTokenでキャンセル処理を書く

  8. Task.Runの正しい使いどころを理解する

  9. .Result.Wait()async voidなどの注意点を学ぶ

  10. 必要に応じてConfigureAwait(false)を学ぶ

この順番で学ぶと、C#のTaskを実務でも使いやすくなります。

まとめ

C#のTaskは、非同期処理を表すための重要な型です。値を返さない非同期処理にはTask、値を返す非同期処理にはTask<T>を使います。

async/awaitを使うと、非同期処理を通常の処理の流れに近い形で書けます。awaitTaskの完了を待ち、完了後に続きの処理を再開します。

C#のTaskを学ぶうえで、特に重要なポイントは次のとおりです。

  • Taskは非同期処理の完了を表すオブジェクト

  • Task<T>は非同期処理の結果を受け取るために使う

  • asyncは非同期メソッドを定義するキーワード

  • awaitTaskの完了を非同期的に待つキーワード

  • Task.WhenAllで複数の処理をまとめて待てる

  • Task.WhenAnyで最初に完了した処理を受け取れる

  • CancellationTokenでキャンセル可能な処理を書ける

  • Task.Runは主にCPUバウンド処理に使う

  • I/O処理ではTask.Runではなく非同期APIを直接使う

  • async void.Result.Wait()はできるだけ避ける

初心者のうちは、まずasync Taskasync Task<T>の形に慣れることが大切です。そのうえで、Task.WhenAllCancellationToken、例外処理、Task.Runの使いどころを順番に学んでいくと、C#の非同期処理を安全で読みやすく書けるようになります。