C# Task.Runの使い方を初心者向けに解説|async/awaitとの違い・戻り値・例外処理までわかる完全ガイド

はじめに

C#で非同期処理について学び始めると、よく目にするのがTask.Runです。

Task.Runは、重い処理を別のスレッドで実行したいときや、画面のフリーズを防ぎたいときに使われる便利なメソッドです。しかし、初心者のうちは「async/awaitと何が違うの?」「戻り値はどう受け取るの?」「例外はどこでcatchすればいいの?」と迷いやすいポイントがたくさんあります。

この記事では、C#のTask.Runの使い方を初心者向けにわかりやすく解説します。基本構文から、async/awaitとの違い、戻り値の受け取り方、例外処理、キャンセル処理、実践的なコード例、よくあるエラーまで順番に見ていきましょう。

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

1-1. Task.Runは「処理を別スレッドで実行する」ためのメソッド

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

簡単に言うと、今の処理とは別の場所で重い処理を動かしたいときに使います。

C#
await Task.Run(() =>
{
// 別スレッドで実行したい処理
});

たとえば、時間のかかる計算処理をメインスレッドでそのまま実行すると、アプリケーションが一時的に止まったように見えることがあります。特にWindows FormsやWPFなどのUIアプリでは、メインスレッドが画面描画も担当しているため、重い処理を実行すると画面が固まってしまいます。

そのようなときにTask.Runを使うと、重い処理を別スレッドに逃がすことができます。

1-2. Task.Runを使うと何がうれしいのか

Task.Runを使うメリットは、主に次のとおりです。

重い処理を別スレッドで実行できるため、メイン処理を止めにくくなります。UIアプリでは画面のフリーズを防ぎやすくなり、ユーザーにとって操作しやすいアプリになります。

また、複数の独立した処理を並列に実行したい場合にも使えます。たとえば、複数の計算処理を同時に実行し、最後に結果をまとめて受け取るような書き方ができます。

ただし、Task.Runを使えば必ず速くなるわけではありません。処理内容や実行環境によっては、スレッドの切り替えコストが増えて逆に遅くなることもあります。

1-3. Task.Runが使われる代表的な場面

Task.Runがよく使われるのは、CPUを多く使う重い処理です。

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

画像の加工、大量データの集計、複雑な計算、圧縮や展開、暗号化、同期的にしか提供されていない重いライブラリ処理などです。

一方で、ファイル読み込み、データベースアクセス、Web API呼び出しのようなI/O処理では、Task.Runよりも専用の非同期APIを使うのが基本です。

たとえば、Web APIを呼び出すならTask.Runで包むのではなく、HttpClient.GetAsyncのような非同期メソッドを使います。

1-4. Task.Runを理解する前に知っておきたいTaskの基本

Taskは、C#で非同期処理を表すための型です。

Taskは「これから完了する処理」を表します。処理がまだ実行中の場合もあれば、すでに完了している場合もあります。

戻り値がない非同期処理はTaskで表します。

C#
Task task = SomeAsyncMethod();

戻り値がある非同期処理はTask<T>で表します。

C#
Task<int> task = GetNumberAsync();

そして、awaitを使うと、そのTaskが完了するまで待ち、戻り値がある場合は結果を受け取れます。

C#
int result = await GetNumberAsync();

Task.Runも戻り値としてTaskまたはTask<T>を返します。そのため、awaitと組み合わせて使うことが多いです。

2. Task.Runの基本的な使い方

2-1. Task.Runの基本構文

Task.Runの基本構文は次のとおりです。

C#
Task.Run(() =>
{
// 実行したい処理
});

Task.Runには、実行したい処理をラムダ式で渡します。

処理の完了を待ちたい場合は、awaitを付けます。

C#
await Task.Run(() =>
{
// 実行したい処理
});

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

C#
int result = await Task.Run(() =>
{
return 100;
});

2-2. 戻り値なしの処理をTask.Runで実行する

戻り値がない処理をTask.Runで実行する例を見てみましょう。

C#
await Task.Run(() =>
{
Console.WriteLine("重い処理を開始します");

for (int i = 0; i < 100000000; i++)
{
// 何らかの重い処理
}

Console.WriteLine("重い処理が完了しました");
});

このコードでは、Task.Runの中に書いた処理が別スレッドで実行されます。

awaitを付けているため、Task.Runの処理が完了するまで次の処理には進みません。ただし、await中は呼び出し元のスレッドをブロックしないため、UIアプリなどでは画面が固まりにくくなります。

2-3. ラムダ式を使ったTask.Runの書き方

Task.Runでは、ラムダ式を使う書き方がよく使われます。

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

処理が1行だけなら、さらに短く書けます。

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

戻り値がある場合も、ラムダ式で簡単に書けます。

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

ラムダ式を使うと、Task.Runに渡したい処理をその場で簡潔に表現できます。

2-4. メソッドをTask.Runに渡す書き方

ラムダ式を使わず、既存のメソッドをTask.Runに渡すこともできます。

C#
await Task.Run(DoHeavyWork);

この場合、DoHeavyWorkは戻り値なしのメソッドです。

C#
void DoHeavyWork()
{
for (int i = 0; i < 100000000; i++)
{
// 重い処理
}
}

戻り値があるメソッドも渡せます。

C#
int result = await Task.Run(Calculate);
C#
int Calculate()
{
return 100;
}

メソッド名を直接渡す書き方は、処理内容が別メソッドに整理されている場合に読みやすくなります。

2-5. Task.Runをawaitして処理完了を待つ

Task.Runは基本的にawaitとセットで使うのがおすすめです。

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

Console.WriteLine("Task.Runの処理が完了しました");

awaitを付けることで、Task.Runの中の処理が完了した後に次の処理へ進みます。

一方、awaitを付けないと、処理の完了を待たずに次の行へ進みます。

C#
Task.Run(() =>
{
DoHeavyWork();
});

Console.WriteLine("この行は先に実行される可能性があります");

このような書き方は、意図しない処理順になったり、例外を見逃したりする原因になります。初心者のうちは、特別な理由がない限りawait Task.Run(...)の形で書くとよいでしょう。

3. Task.Runとasync/awaitの違い

3-1. Task.Runとasync/awaitは役割が違う

Task.Runasync/awaitは、どちらも非同期処理でよく使われますが、役割は違います。

Task.Runは、処理をThreadPool上で実行するためのメソッドです。つまり、処理を別スレッドに渡す役割があります。

一方、async/awaitは、非同期処理を読みやすく書くための構文です。awaitTaskの完了を待つために使います。

つまり、Task.Runは「どこで処理を実行するか」に関係し、async/awaitは「非同期処理をどう待つか」に関係します。

3-2. async/awaitだけでは別スレッドで実行されるとは限らない

初心者が誤解しやすいポイントとして、async/awaitを使えば必ず別スレッドで実行されるわけではない、という点があります。

たとえば、次のようなコードを考えます。

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

このコードは非同期処理ですが、Task.RunのようにCPU処理を別スレッドに移しているわけではありません。ファイル読み込みの完了を非同期に待っているだけです。

async/awaitは、あくまで非同期処理を自然な書き方で待つための仕組みです。別スレッドで処理を実行したい場合は、Task.Runのような仕組みを使います。

3-3. Task.Runを使うべき処理とawaitだけでよい処理

Task.Runを使うべき処理は、CPUを長時間使う同期処理です。

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

このように、重い計算処理を別スレッドで実行したい場合にはTask.Runが向いています。

一方、すでに非同期APIが用意されている処理は、Task.Runで包む必要はありません。

C#
string html = await httpClient.GetStringAsync("https://example.com");

このようなWeb通信は、Task.RunではなくGetStringAsyncのような非同期メソッドをそのままawaitします。

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

Task.Runを理解するうえで大切なのが、CPUバウンド処理とI/Oバウンド処理の違いです。

CPUバウンド処理とは、CPUの計算能力を多く使う処理です。画像処理、大量データの計算、暗号化、圧縮などが該当します。CPUバウンド処理は、Task.Runで別スレッドに移すことで、UIスレッドなどをブロックしにくくできます。

I/Oバウンド処理とは、外部の応答待ちが中心になる処理です。ファイル、ネットワーク、データベースなどへのアクセスが該当します。I/Oバウンド処理では、専用の非同期APIを使うのが基本です。

つまり、重い計算にはTask.Run、通信やファイル操作には非同期API、という考え方が基本になります。

3-5. Task.Runとawaitを組み合わせる基本パターン

Task.Runawaitを組み合わせる基本パターンは次のとおりです。

C#
public async Task DoWorkAsync()
{
await Task.Run(() =>
{
DoHeavyWork();
});
}

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

C#
public async Task<int> CalculateAsync()
{
int result = await Task.Run(() =>
{
return HeavyCalculation();
});

return result;
}

ポイントは、Task.Runで処理を別スレッドに渡し、awaitでその完了を待つことです。この形を覚えておくと、多くの場面で応用できます。

4. Task.Runで戻り値を受け取る方法

4-1. Task.Runで値を返す基本構文

Task.Runでは、処理の結果を戻り値として受け取ることができます。

C#
int result = await Task.Run(() =>
{
return 10 + 20;
});

Console.WriteLine(result);

このコードでは、Task.Runの中で計算した結果30を、awaitによって受け取っています。

1行で書くこともできます。

C#
int result = await Task.Run(() => 10 + 20);

4-2. Task<T>とは何か

戻り値があるTaskは、Task<T>という型で表されます。

たとえば、整数を返す非同期処理はTask<int>です。

C#
Task<int> task = Task.Run(() => 100);

文字列を返す場合はTask<string>です。

C#
Task<string> task = Task.Run(() => "Hello");

Tの部分には、戻り値の型が入ります。

Task<int>は「将来的にint型の結果を返す処理」、Task<string>は「将来的にstring型の結果を返す処理」と考えるとわかりやすいです。

4-3. awaitで戻り値を取得する方法

Task<T>の結果を取得するには、awaitを使います。

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

int result = await task;

このとき、awaitTaskの完了を待ち、完了後に結果を取り出します。

実際のコードでは、次のようにまとめて書くことが多いです。

C#
int result = await Task.Run(() =>
{
return CalculateTotal();
});

awaitを使うことで、非同期処理であっても通常の戻り値のように扱えるのが大きなメリットです。

4-4. 複数の戻り値を扱う場合の考え方

C#のメソッドは基本的に1つの値を返します。複数の値を返したい場合は、クラス、構造体、タプルなどを使います。

タプルを使うと、簡単に複数の値を返せます。

C#
var result = await Task.Run(() =>
{
int count = 10;
int total = 500;
return (count, total);
});

Console.WriteLine(result.count);
Console.WriteLine(result.total);

名前付きのタプルにすると、より読みやすくなります。

C#
var result = await Task.Run(() =>
{
return (Count: 10, Total: 500);
});

Console.WriteLine(result.Count);
Console.WriteLine(result.Total);

データ構造が複雑な場合は、専用のクラスを作るとよいでしょう。

C#
public class CalculationResult
{
public int Count { get; set; }
public int Total { get; set; }
}

4-5. Resultで戻り値を取得する場合の注意点

Task<T>にはResultプロパティがあります。

C#
int result = Task.Run(() => 100).Result;

この書き方でも戻り値を取得できますが、初心者にはおすすめしません。

Resultは処理が完了するまで現在のスレッドをブロックします。UIアプリやASP.NETなどでは、デッドロックやパフォーマンス低下の原因になることがあります。

基本的には、Resultではなくawaitを使いましょう。

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

awaitを使うことで、スレッドを無駄にブロックせず、例外処理も自然に書けます。

5. Task.Runの例外処理

5-1. Task.Run内で発生した例外はどう扱われるか

Task.Runの中で例外が発生した場合、その例外はTaskに保持されます。

C#
Task task = Task.Run(() =>
{
throw new InvalidOperationException("エラーが発生しました");
});

この時点では、例外はTaskの中に記録されています。awaitしたタイミングで、その例外が再スローされます。

C#
await task;

そのため、Task.Runの例外処理では、awaittry-catchを組み合わせるのが基本です。

5-2. try-catchとawaitを使った例外処理

Task.Runで発生した例外を処理する基本形は次のとおりです。

C#
try
{
await Task.Run(() =>
{
throw new InvalidOperationException("エラーが発生しました");
});
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"例外を捕まえました: {ex.Message}");
}

awaitしている部分をtryブロックで囲むことで、Task.Runの中で発生した例外をcatchできます。

実際の処理では、次のように書くことが多いです。

C#
try
{
int result = await Task.Run(() => Calculate());
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"処理に失敗しました: {ex.Message}");
}

初心者のうちは、Task.Runを使ったらawaitし、必要に応じてtry-catchで囲む、と覚えておくとよいでしょう。

5-3. awaitしないTask.Runで例外を見逃す危険性

Task.Runawaitしない場合、例外を見逃す危険があります。

C#
Task.Run(() =>
{
throw new Exception("エラー");
});

このコードでは、Task.Runの中で例外が発生しても、呼び出し元ではすぐに捕まえられません。

次のように書いても、期待どおりにはcatchできません。

C#
try
{
Task.Run(() =>
{
throw new Exception("エラー");
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

なぜなら、Task.RunはすぐにTaskを返して次の処理に進むため、例外が発生するタイミングがtryブロックの外のような扱いになるからです。

正しくは、awaitします。

C#
try
{
await Task.Run(() =>
{
throw new Exception("エラー");
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

5-4. AggregateExceptionとは何か

Taskの例外でよく出てくるのがAggregateExceptionです。

AggregateExceptionは、複数の例外をまとめて扱うための例外です。特に、Wait()Resultを使ってTaskの完了を待った場合に見かけることがあります。

C#
try
{
Task.Run(() =>
{
throw new InvalidOperationException("エラー");
}).Wait();
}
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions)
{
Console.WriteLine(inner.Message);
}
}

一方、awaitを使った場合は、通常は元の例外がそのまま投げられるため、コードが読みやすくなります。

C#
try
{
await Task.Run(() =>
{
throw new InvalidOperationException("エラー");
});
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}

この点でも、初心者にはWait()Resultよりawaitがおすすめです。

5-5. 例外処理で初心者がやりがちなミス

初心者がやりがちなミスのひとつは、Task.Runawaitしないことです。awaitしないと、例外を適切に捕まえられなかったり、処理完了前に次の処理が進んでしまったりします。

もうひとつは、Task.Runの外側だけをtry-catchして安心してしまうことです。awaitしていない場合、外側のtry-catchでは例外を捕まえられません。

また、async voidを使ってしまうのもよくあるミスです。イベントハンドラー以外では、非同期メソッドの戻り値はTaskまたはTask<T>にしましょう。

6. Task.Runで非同期処理を書く実践例

6-1. 重い計算処理をTask.Runで実行する例

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

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;
}

このコードでは、ループによる重い計算をTask.Runの中で実行しています。

呼び出し側は次のように書けます。

C#
int result = await CalculateAsync();
Console.WriteLine(result);

awaitを使っているため、処理の完了を待ちながらも、現在のスレッドを無駄にブロックしにくい書き方になります。

6-2. UIアプリで画面フリーズを防ぐ例

Windows FormsやWPFなどのUIアプリでは、重い処理をUIスレッドで実行すると画面が固まります。

たとえば、ボタンクリック時に重い処理を実行する場合は、次のように書けます。

C#
private async void Button_Click(object sender, EventArgs e)
{
button1.Enabled = false;
label1.Text = "処理中...";

try
{
int result = await Task.Run(() =>
{
return HeavyCalculation();
});

label1.Text = $"結果: {result}";
}
catch (Exception ex)
{
label1.Text = $"エラー: {ex.Message}";
}
finally
{
button1.Enabled = true;
}
}

この例では、重い計算はTask.Run内で実行し、画面の更新はawait後に行っています。

UI部品の操作は、基本的にUIスレッドで行う必要があります。Task.Runの中から直接label1.Textなどを変更しないように注意しましょう。

6-3. 複数のTask.Runを並列実行する例

複数の独立した重い処理を並列に実行したい場合は、複数のTask.Runを作成できます。

C#
Task<int> task1 = Task.Run(() => CalculateA());
Task<int> task2 = Task.Run(() => CalculateB());
Task<int> task3 = Task.Run(() => CalculateC());

int result1 = await task1;
int result2 = await task2;
int result3 = await task3;

Console.WriteLine(result1 + result2 + result3);

このコードでは、CalculateACalculateBCalculateCが並列に実行されます。

ただし、たくさんのTask.Runを一度に作りすぎると、ThreadPoolに負荷がかかります。処理の数が多い場合は、並列数を制限することも考えましょう。

6-4. Task.WhenAllと組み合わせる例

複数のTaskをまとめて待つには、Task.WhenAllが便利です。

C#
Task<int> task1 = Task.Run(() => CalculateA());
Task<int> task2 = Task.Run(() => CalculateB());
Task<int> task3 = Task.Run(() => CalculateC());

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

int total = results.Sum();
Console.WriteLine(total);

Task.WhenAllは、すべてのTaskが完了するまで待ちます。

複数の処理を並列に実行し、すべての結果をまとめて受け取りたい場合に使いやすい方法です。

6-5. 進捗表示とTask.Runを組み合わせる例

重い処理を実行している間、進捗を表示したい場合があります。そのようなときは、IProgress<T>を使うと安全に進捗を通知できます。

C#
public async Task StartAsync()
{
var progress = new Progress<int>(value =>
{
Console.WriteLine($"進捗: {value}%");
});

await Task.Run(() =>
{
DoWork(progress);
});
}

private void DoWork(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
progress.Report(i);
}
}

Progress<T>を使うと、UIアプリでも比較的安全に進捗表示を更新できます。

Task.Runの中で重い処理を行い、進捗だけを外側へ通知する、という形にするとコードが整理しやすくなります。

7. Task.Runを使うべきケース・使わないほうがよいケース

7-1. Task.Runが向いているケース

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

たとえば、次のようなケースです。

画像処理、大量データの集計、複雑な計算、暗号化、圧縮処理、既存の同期ライブラリが重い処理を行う場合などです。

特にUIアプリでは、重い処理をUIスレッドから切り離すためにTask.Runがよく使われます。

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

このように、重い同期処理を別スレッドに逃がす目的で使うのが基本です。

7-2. Task.Runが不要なケース

すでに非同期メソッドが用意されている処理では、Task.Runは不要です。

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

C#
string text = await File.ReadAllTextAsync("sample.txt");
C#
string html = await httpClient.GetStringAsync("https://example.com");
C#
var data = await dbContext.Users.ToListAsync();

これらは、専用の非同期APIをそのままawaitすれば十分です。

次のように、非同期APIをわざわざTask.Runで包む必要はありません。

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

このような書き方は無駄に複雑で、ThreadPoolを余計に使ってしまう可能性があります。

7-3. ASP.NET CoreでTask.Runを安易に使わないほうがよい理由

ASP.NET Coreでは、Task.Runを安易に使わないほうがよい場面が多いです。

ASP.NET Coreは、多数のリクエストを効率よく処理するためにThreadPoolを使っています。そこで各リクエスト内でTask.Runを多用すると、ThreadPoolのスレッドを余計に消費し、全体のスループットが悪化する可能性があります。

たとえば、次のような書き方は基本的には避けたほうがよいです。

C#
public async Task<IActionResult> Index()
{
var data = await Task.Run(() => GetDataFromDatabase());
return View(data);
}

データベースアクセスなら、非同期APIを使うべきです。

C#
public async Task<IActionResult> Index()
{
var data = await dbContext.Users.ToListAsync();
return View(data);
}

ASP.NET CoreでTask.Runを使う場合は、本当にCPUバウンドな処理をリクエスト内で実行する必要があるのか慎重に考えましょう。

7-4. I/O処理ではTask.Runより非同期APIを使う

I/O処理では、Task.Runよりも専用の非同期APIを使うのが基本です。

ファイル読み込みなら、ReadAllTextAsyncReadAsyncを使います。

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

HTTP通信なら、HttpClientの非同期メソッドを使います。

C#
string html = await httpClient.GetStringAsync("https://example.com");

データベースアクセスなら、利用しているライブラリの非同期メソッドを使います。

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

I/O処理は待ち時間が中心です。わざわざ別スレッドを使って待つよりも、非同期APIで効率よく待つほうが適しています。

7-5. ThreadPoolを無駄に消費しないための注意点

Task.RunThreadPoolを使います。そのため、Task.Runを大量に使うと、ThreadPoolのスレッドを消費します。

特に、長時間ブロックする処理を大量にTask.Runで実行すると、他の処理に必要なスレッドが不足する可能性があります。

次のような使い方には注意が必要です。

C#
for (int i = 0; i < 10000; i++)
{
Task.Run(() => DoWork());
}

大量の処理を並列実行したい場合は、並列数を制限する設計が必要です。SemaphoreSlimParallel.ForEachAsyncなどを検討するとよいでしょう。

8. Task.RunとThread・ThreadPoolの違い

8-1. Task.RunとThreadの違い

Threadは、明示的に新しいスレッドを作成して処理を実行するためのクラスです。

C#
Thread thread = new Thread(() =>
{
DoWork();
});

thread.Start();

一方、Task.RunThreadPoolを使って処理を実行します。

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

Threadはスレッドを細かく制御できますが、その分扱いが難しくなります。Task.Runは簡潔に書けて、awaitとも組み合わせやすいため、通常の非同期処理では使いやすい選択肢です。

8-2. Task.RunとThreadPoolの関係

Task.Runは、内部的にはThreadPoolを利用して処理を実行します。

ThreadPoolは、再利用可能なスレッドの集まりです。処理のたびに新しいスレッドを作るのではなく、既存のスレッドを再利用することで、スレッド作成のコストを減らします。

つまり、Task.Runは「ThreadPoolにこの処理を実行してほしい」と依頼するようなイメージです。

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

このコードでは、DoWorkThreadPool上のスレッドで実行されます。

8-3. Task.Runは新しいスレッドを毎回作るわけではない

Task.Runを呼び出すたびに、新しいスレッドが必ず作られるわけではありません。

Task.RunThreadPoolを利用するため、すでに存在するスレッドが再利用されることがあります。

そのため、Task.Runnew Thread(...)より手軽で効率的に使える場面が多いです。

ただし、ThreadPoolのスレッド数には限りがあります。Task.Runを使いすぎると、スレッドが不足して処理待ちが発生する可能性があります。

8-4. Task.RunとTask.Factory.StartNewの違い

Task.Factory.StartNewTaskを開始するためのメソッドです。

C#
Task.Factory.StartNew(() =>
{
DoWork();
});

しかし、Task.Factory.StartNewはオプションが多く、スケジューラーの指定なども関係するため、初心者には少し難しい面があります。

通常の用途では、Task.Runのほうがシンプルです。

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

Task.Runは、一般的なバックグラウンド処理を簡潔に書くためのメソッドと考えるとよいでしょう。

8-5. 初心者はTask.Runを選べばよいのか

初心者が「重い同期処理を別スレッドで実行したい」と考えた場合は、まずTask.Runを使うのがわかりやすいです。

ただし、何でもTask.Runで包めばよいわけではありません。

CPUバウンドな重い処理にはTask.Run、I/Oバウンドな処理には非同期API、という使い分けが重要です。

迷ったときは、まず次のように考えるとよいでしょう。

その処理が計算中心ならTask.Runを検討します。通信やファイル、データベースの待ち時間が中心なら、専用の非同期メソッドを探します。

9. Task.Runでキャンセル処理を実装する方法

9-1. CancellationTokenとは何か

CancellationTokenは、非同期処理や長時間処理をキャンセルするための仕組みです。

たとえば、ユーザーが「キャンセル」ボタンを押したときに、実行中の処理を途中で止めたい場合に使います。

CancellationTokenは、キャンセル要求が出されたかどうかを処理側に伝えるためのものです。

キャンセルを発行する側では、CancellationTokenSourceを使います。

C#
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

キャンセルしたいときは、次のようにします。

C#
cts.Cancel();

9-2. Task.RunにCancellationTokenを渡す方法

Task.RunにはCancellationTokenを渡せます。

C#
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task task = Task.Run(() =>
{
DoWork(token);
}, token);

ただし、Task.RunCancellationTokenを渡しただけで、実行中の処理が自動的に止まるわけではありません。

処理の中でキャンセル要求を確認する必要があります。

9-3. 処理中にキャンセルを確認する方法

処理中にキャンセルを確認するには、ThrowIfCancellationRequestedを使います。

C#
await Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();

// 時間のかかる処理
Thread.Sleep(100);
}
}, token);

ThrowIfCancellationRequestedは、キャンセルが要求されている場合にOperationCanceledExceptionを投げます。

例外を投げずに確認だけしたい場合は、IsCancellationRequestedを使います。

C#
if (token.IsCancellationRequested)
{
return;
}

ただし、キャンセルを呼び出し元に正しく伝えたい場合は、ThrowIfCancellationRequestedを使うことが多いです。

9-4. OperationCanceledExceptionの扱い方

キャンセル時には、OperationCanceledExceptionが発生します。

C#
try
{
await Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
Thread.Sleep(100);
}
}, token);
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました");
}

キャンセルは異常終了というより、想定された終了パターンです。そのため、通常のエラーとは分けて扱うとよいでしょう。

C#
try
{
await DoWorkAsync(token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}

このように、OperationCanceledExceptionを先にcatchし、その後に一般的なExceptionをcatchすると整理しやすいです。

9-5. キャンセル処理を書くときの注意点

キャンセル処理を書くときは、CancellationTokenを渡すだけで満足しないことが大切です。

実行中のループや長時間処理の途中で、定期的にキャンセル要求を確認する必要があります。

また、キャンセルできるタイミングを細かくしすぎるとコードが複雑になります。逆に、確認頻度が少なすぎると、キャンセルしてもなかなか止まらない処理になります。

処理の区切りごとにtoken.ThrowIfCancellationRequested()を入れると、バランスのよいキャンセル処理を書きやすくなります。

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

10-1. awaitを付け忘れて処理順がおかしくなる

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

C#
Task.Run(() =>
{
DoWork();
});

Console.WriteLine("完了しました");

このコードでは、DoWorkが完了する前に「完了しました」と表示される可能性があります。

処理完了後に次へ進みたい場合は、awaitを付けます。

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

Console.WriteLine("完了しました");

Task.Runを使ったら、その処理を待つ必要があるかどうかを必ず考えましょう。

10-2. ResultやWaitでデッドロックする

Taskの完了を待つ方法として、ResultWait()があります。

C#
var result = Task.Run(() => Calculate()).Result;
C#
Task.Run(() => DoWork()).Wait();

しかし、これらは現在のスレッドをブロックします。UIアプリや一部の環境では、デッドロックの原因になることがあります。

基本的には、ResultWait()ではなくawaitを使いましょう。

C#
var result = await Task.Run(() => Calculate());

awaitを使うことで、非同期処理を自然に待つことができます。

10-3. async voidを使って例外が捕まらない

非同期メソッドを作るとき、初心者がやりがちなのがasync voidです。

C#
public async void DoWorkAsync()
{
await Task.Run(() => DoWork());
}

async voidは、呼び出し元が完了を待てず、例外処理もしにくくなります。

イベントハンドラーを除き、非同期メソッドの戻り値はTaskまたはTask<T>にしましょう。

C#
public async Task DoWorkAsync()
{
await Task.Run(() => DoWork());
}

戻り値がある場合は、Task<T>を使います。

C#
public async Task<int> CalculateAsync()
{
return await Task.Run(() => Calculate());
}

10-4. UIスレッドを直接操作してエラーになる

UIアプリでは、Task.Runの中からUI部品を直接操作するとエラーになることがあります。

悪い例です。

C#
await Task.Run(() =>
{
label1.Text = "処理中";
});

UI部品は基本的にUIスレッドから操作する必要があります。

正しくは、重い処理だけをTask.Runに入れ、UI更新はawaitの前後で行います。

C#
label1.Text = "処理中";

int result = await Task.Run(() =>
{
return HeavyCalculation();
});

label1.Text = $"結果: {result}";

このように書くと、UIスレッドとバックグラウンド処理の役割を分けられます。

10-5. Task.Runを多用して逆に遅くなる

Task.Runを使えば必ず速くなるわけではありません。

軽い処理を大量にTask.Runで実行すると、スレッドの切り替えやタスク管理のコストが増え、逆に遅くなることがあります。

悪い例です。

C#
foreach (var item in items)
{
await Task.Run(() => Process(item));
}

この場合、1件ずつTask.Runして待っているため、かえって効率が悪いことがあります。

処理が軽いならそのまま実行し、重い処理をまとめて実行する設計にしたほうがよい場合があります。

C#
await Task.Run(() =>
{
foreach (var item in items)
{
Process(item);
}
});

また、大量の並列処理が必要な場合は、並列数の制限を検討しましょう。

11. Task.Runのベストプラクティス

11-1. 基本はawaitとセットで使う

Task.Runは、基本的にawaitとセットで使いましょう。

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

awaitすることで、処理完了を正しく待てます。また、例外もtry-catchで扱いやすくなります。

awaitしないTask.Runは、処理順が不明確になり、例外も見逃しやすくなります。明確な理由がない限り避けましょう。

11-2. asyncメソッドの戻り値はTaskまたはTask<T>にする

非同期メソッドの戻り値は、戻り値がなければTask、戻り値があればTask<T>にします。

C#
public async Task SaveAsync()
{
await Task.Run(() => Save());
}
C#
public async Task<int> CalculateAsync()
{
return await Task.Run(() => Calculate());
}

async voidは、イベントハンドラー以外では基本的に使わないようにしましょう。

11-3. 重い同期処理だけをTask.Runに入れる

Task.Runに入れるべきなのは、重い同期処理です。

C#
var result = await Task.Run(() =>
{
return HeavyCalculation();
});

逆に、軽い処理や単純な代入、すでに非同期APIがある処理をTask.Runに入れる必要はありません。

Task.Runは便利ですが、使いどころを間違えるとコードが複雑になり、パフォーマンスも悪化する可能性があります。

11-4. I/O処理は専用の非同期メソッドを使う

ファイル、ネットワーク、データベースなどのI/O処理では、専用の非同期メソッドを使いましょう。

C#
string text = await File.ReadAllTextAsync("sample.txt");
C#
string html = await httpClient.GetStringAsync("https://example.com");
C#
var users = await dbContext.Users.ToListAsync();

I/O処理をTask.Runで包むのではなく、ライブラリが提供している非同期APIを使うのが基本です。

11-5. 例外処理とキャンセル処理を忘れない

Task.Runを使うときは、例外処理とキャンセル処理も意識しましょう。

例外処理は、awaittry-catchで書きます。

C#
try
{
await Task.Run(() => DoWork());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

キャンセルが必要な処理では、CancellationTokenを使います。

C#
await Task.Run(() =>
{
token.ThrowIfCancellationRequested();
DoWork();
}, token);

長時間実行される処理では、ユーザーがキャンセルできる設計にしておくと使いやすいアプリになります。

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

12-1. Task.Runは必ず別スレッドで動く?

Task.Runは、通常ThreadPool上のスレッドで処理を実行します。

ただし、「毎回新しいスレッドを作る」という意味ではありません。ThreadPoolの既存スレッドが再利用されることがあります。

そのため、Task.Runは「新しいスレッドを作るメソッド」ではなく、「ThreadPoolに処理を渡すメソッド」と考えるとよいです。

12-2. Task.Runとawaitはどちらを使えばいい?

Task.Runawaitは比較するものではなく、役割が違います。

Task.Runは処理を別スレッドで実行するために使います。awaitTaskの完了を待つために使います。

重い同期処理を別スレッドで実行したい場合は、次のように組み合わせます。

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

すでに非同期APIがある場合は、Task.Runを使わずにそのままawaitします。

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

12-3. Task.Runをawaitしないとどうなる?

Task.Runawaitしないと、処理の完了を待たずに次の処理へ進みます。

C#
Task.Run(() => DoWork());
Console.WriteLine("次の処理");

この場合、DoWorkが終わる前に「次の処理」が実行される可能性があります。

また、Task.Run内で発生した例外を見逃しやすくなります。

処理完了を待ちたい場合や例外を扱いたい場合は、awaitしましょう。

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

12-4. Task.Runで戻り値を返すには?

Task.Runの中で値をreturnし、awaitで受け取ります。

C#
int result = await Task.Run(() =>
{
return 100;
});

1行で書くこともできます。

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

戻り値がある場合、Task.RunTask<T>を返します。awaitするとT型の値を取得できます。

12-5. Task.Runの例外はどこでcatchすればいい?

Task.Runの例外は、awaitする場所をtry-catchで囲んで処理します。

C#
try
{
await Task.Run(() =>
{
throw new Exception("エラー");
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

awaitしない場合、例外を正しくcatchできないことがあります。

基本は、await Task.Run(...)try-catchで囲む、と覚えておきましょう。

12-6. Task.Runはパフォーマンス改善になる?

Task.Runは、使い方によってはパフォーマンス改善につながります。

特に、UIアプリで重い処理を別スレッドに移すことで、画面のフリーズを防げます。また、複数のCPUバウンド処理を並列に実行することで、処理時間を短縮できる場合もあります。

ただし、必ず速くなるわけではありません。軽い処理をTask.Runで包むと、タスク管理やスレッド切り替えのコストで逆に遅くなることがあります。

Task.Runは、重い同期処理に絞って使うのが基本です。

12-7. Task.RunはASP.NET Coreで使ってもいい?

ASP.NET CoreでもTask.Runは使えますが、安易に使うのはおすすめしません。

ASP.NET Coreでは、多数のリクエストを効率よく処理する必要があります。そこでTask.Runを多用すると、ThreadPoolのスレッドを余計に消費し、全体の性能が下がる可能性があります。

データベースアクセスやHTTP通信などは、Task.Runではなく非同期APIを使いましょう。

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

本当にCPUバウンドな重い処理がある場合だけ、設計を考えたうえでTask.Runを検討するとよいです。

まとめ

C#のTask.Runは、重い同期処理をThreadPool上の別スレッドで実行したいときに使うメソッドです。

基本的な使い方は、次のようにawaitと組み合わせる形です。

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

戻り値がある場合は、Task<T>として扱い、awaitで結果を受け取ります。

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

Task.Runasync/awaitは役割が異なります。Task.Runは処理を別スレッドで実行するためのもの、async/awaitは非同期処理を待つための構文です。

また、Task.RunはCPUバウンドな重い処理に向いています。ファイル操作、HTTP通信、データベースアクセスなどのI/Oバウンド処理では、専用の非同期APIを使うのが基本です。

例外処理では、awaitする箇所をtry-catchで囲みます。

C#
try
{
await Task.Run(() => DoWork());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

キャンセルが必要な場合は、CancellationTokenを使って、処理中にキャンセル要求を確認します。

Task.Runは便利な機能ですが、何でも包めばよいわけではありません。重い同期処理だけを対象にし、基本はawaitとセットで使い、例外処理やキャンセル処理も忘れないようにしましょう。