C#で別スレッドを使う方法|Thread・Task・async/awaitの違いと安全な実装例

はじめに

C#で「別スレッド」を使う場面は、UIを固めずに重い処理を実行したいとき、複数の処理を並列に進めたいとき、ファイル読み込みやAPI通信などの待ち時間を効率よく扱いたいときです。

ただし、C#には ThreadTaskasync/awaitThreadPool など複数の選択肢があり、「どれを使えばよいのか」「本当に別スレッドで動いているのか」「UIを更新してもよいのか」で迷いやすいです。

結論から言うと、現在のC#開発では、基本的に Taskasync/await を中心に考えるのがおすすめです。Thread はスレッドそのものを直接扱いたい特殊なケースで使い、UIアプリではUIスレッドへの戻し方、共有データの排他制御、キャンセル処理、例外処理まで含めて設計する必要があります。

1. C#で別スレッドを使うとは?検索ユーザーが知りたい基本

1-1. 別スレッドとは何か

別スレッドとは、現在実行中の処理とは別の実行単位でコードを動かすことです。C#では通常、アプリケーションの開始時にメインスレッドが動き、その上でコードが順番に実行されます。そこから別のスレッドを作ると、メインスレッドとは独立して別の処理を進められます。

たとえば、メインスレッドで画面表示を担当しながら、別スレッドで重い計算を実行する、といった使い方ができます。Thread クラスはスレッドの作成、制御、状態取得などを行うためのクラスです。Microsoftのドキュメントでも、Thread クラスはスレッドを作成・制御し、優先度や状態を扱うものとして説明されています。Microsoft Learn

1-2. メインスレッドとワーカースレッドの違い

メインスレッドは、アプリケーションの中心となるスレッドです。コンソールアプリなら Main メソッドを実行するスレッド、Windows FormsやWPFなどのUIアプリなら画面操作やイベント処理を担当するUIスレッドが重要になります。

一方、ワーカースレッドは、メイン処理を補助する作業用スレッドです。重い計算、ファイル処理、データ変換、バックグラウンド処理などを任せることで、メインスレッドの負担を減らせます。

ただし、別スレッドを使えば必ず速くなるわけではありません。スレッドの作成や切り替えにもコストがあり、共有データの扱いを誤るとバグの原因になります。

1-3. 別スレッドを使う主な目的

C#で別スレッドを使う主な目的は次の通りです。

  • UIのフリーズを防ぐ

  • 重い計算処理をバックグラウンドで実行する

  • 複数の処理を並列に進める

  • ファイル読み込みやAPI通信の待ち時間を効率よく扱う

  • 定期実行や監視処理を裏側で動かす

特にUIアプリでは、ボタンクリック時に時間のかかる処理をそのままUIスレッドで実行すると、画面が固まったように見えます。このような場合に、別スレッドや非同期処理を使います。

1-4. 「別スレッド」と「非同期処理」の違い

「別スレッド」と「非同期処理」は似ていますが、同じ意味ではありません。

別スレッドは、実際に別のスレッド上でコードを実行することです。たとえば new Thread(...)Task.Run(...) を使うと、処理が別スレッドまたはスレッドプール上で実行されます。

非同期処理は、処理の完了を待つ間に呼び出し元をブロックしない書き方です。async/await を使ったからといって、必ず別スレッドが作られるわけではありません。I/O処理では、待機中にスレッドを占有せず、完了後に続きの処理を再開するのが基本です。Microsoftの非同期プログラミング解説でも、await はタスク完了まで呼び出し元へ制御を戻す仕組みとして説明されています。Microsoft Learn

2. C#で別スレッドを使う代表的な方法

2-1. Threadクラスを使う方法

Thread クラスは、スレッドを直接作成して開始する方法です。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
Thread thread = new Thread(Work);
thread.Start();

Console.WriteLine("メインスレッドの処理");
}

static void Work()
{
Console.WriteLine("別スレッドの処理");
}
}

Thread はスレッドを明示的に作成できるため、スレッドの寿命やバックグラウンド化を細かく制御したい場合に使えます。ただし、現代的なC#では、単純な並列処理や非同期処理には Task の方が扱いやすいです。

2-2. Taskクラスを使う方法

Task は、非同期または並列に実行される処理を表すクラスです。Task.Run を使うと、CPU負荷の高い処理をスレッドプール上で実行できます。

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

class Program
{
static async Task Main()
{
await Task.Run(() =>
{
Console.WriteLine("Taskで別スレッド処理");
});

Console.WriteLine("完了");
}
}

Task.Run は、処理をキューに入れて実行するための代表的な方法です。Microsoftのドキュメントでも、Task.Run は指定された処理を実行するタスクを返すメソッドとして説明されています。Microsoft Learn

2-3. async/awaitを使う方法

async/await は、非同期処理を読みやすく書くための構文です。

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

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

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

Console.WriteLine(html.Length);
}
}

この例では、HTTP通信の完了を await しています。待っている間に呼び出し元スレッドをブロックしないため、UIアプリでは画面が固まりにくくなります。

2-4. ThreadPoolを使う方法

ThreadPool は、.NETが管理するワーカースレッドのプールです。毎回新しいスレッドを作るのではなく、既存のスレッドを再利用します。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine("ThreadPoolで実行");
});

Console.ReadLine();
}
}

ただし、通常は ThreadPool を直接使うより、内部でスレッドプールを利用する Task.Run を使う方がコードを書きやすく、戻り値、例外、キャンセルも扱いやすくなります。Microsoftのドキュメントでも、スレッドプールはワーカースレッド管理の仕組みとして説明されています。Microsoft Learn

2-5. BackgroundWorkerは現在も使うべきか

BackgroundWorker は、Windows Formsなどで古くから使われてきたバックグラウンド処理用のクラスです。別スレッドで処理を実行し、進捗通知やキャンセルを扱えます。Microsoftのドキュメントでも、BackgroundWorker は別スレッドで操作を実行するクラスとして説明されています。Microsoft Learn

ただし、新規開発では基本的に Taskasync/await を使う方がおすすめです。既存のWindows Formsアプリを保守する場合は BackgroundWorker を読む機会がありますが、新しく書くなら Task.RunProgress<T>CancellationToken を組み合わせる方が現代的です。

3. Threadクラスで別スレッドを作成する基本例

3-1. Threadを使った最小コード例

Thread を使う最小コードは次の通りです。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();

Console.WriteLine("Main側の処理");
}

static void DoWork()
{
Console.WriteLine("別スレッドで実行中");
}
}

new Thread(DoWork) で実行するメソッドを指定し、Start() で開始します。Thread はスレッドを直接扱うため、単純なコードでも「どの処理がどのスレッドで動くか」を意識する必要があります。

3-2. ラムダ式で処理を渡す方法

短い処理であれば、ラムダ式で書けます。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
Thread thread = new Thread(() =>
{
Console.WriteLine("ラムダ式で別スレッド処理");
});

thread.Start();
}
}

メソッドを別に用意しなくてよいので簡潔です。ただし、処理が長くなる場合は、可読性のためにメソッドへ切り出す方がよいです。

3-3. 引数を別スレッドに渡す方法

Thread.Start に引数を渡す方法もあります。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start("こんにちは");
}

static void DoWork(object? value)
{
string message = value as string ?? "";
Console.WriteLine(message);
}
}

ただし、この方法では引数が object になるため、型安全性が下がります。C#では、ラムダ式で変数をキャプチャする方が自然です。

C#
string message = "こんにちは";

Thread thread = new Thread(() =>
{
Console.WriteLine(message);
});

thread.Start();

3-4. IsBackgroundでバックグラウンドスレッドにする方法

Thread には IsBackground プロパティがあります。バックグラウンドスレッドにすると、フォアグラウンドスレッドがすべて終了した時点でプロセス終了の対象になります。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
Thread thread = new Thread(() =>
{
while (true)
{
Console.WriteLine("バックグラウンド処理中");
Thread.Sleep(1000);
}
});

thread.IsBackground = true;
thread.Start();

Console.WriteLine("Enterで終了");
Console.ReadLine();
}
}

ログ監視や補助的な処理では便利ですが、強制的に終了される可能性があるため、ファイル書き込みやDB更新など中断されると困る処理には注意が必要です。

3-5. Thread.Sleepを使う場合の注意点

Thread.Sleep は、現在のスレッドを指定時間停止します。

C#
Thread.Sleep(1000);

一見便利ですが、UIスレッドで使うと画面が固まります。また、非同期メソッドの中では Thread.Sleep ではなく Task.Delay を使うのが基本です。

C#
await Task.Delay(1000);

Thread.Sleep はスレッド自体を止めますが、Task.Delay は非同期的に待機できます。待機中にスレッドを占有しにくいため、async/await と相性がよいです。

4. Taskを使った別スレッド処理の実装例

4-1. Task.Runで処理を別スレッドに逃がす方法

重い計算などCPU負荷の高い処理は、Task.Run で別スレッドに逃がせます。

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

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

await Task.Run(() =>
{
long total = 0;
for (int i = 0; i < 100_000_000; i++)
{
total += i;
}

Console.WriteLine(total);
});

Console.WriteLine("終了");
}
}

UIアプリでボタンクリック時に重い処理を実行する場合も、Task.Run を使うとUIスレッドをブロックしにくくなります。

4-2. Task.Waitとawaitの違い

Task.Wait() は、タスクが完了するまで現在のスレッドをブロックします。

C#
Task task = Task.Run(() =>
{
Thread.Sleep(1000);
});

task.Wait();
Console.WriteLine("完了");

一方、await は非同期的に完了を待ちます。

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

Console.WriteLine("完了");

UIアプリでは、Wait().Result を使うとUIスレッドをブロックし、フリーズやデッドロックの原因になることがあります。基本的には await を使いましょう。

4-3. 戻り値を受け取るTask<T>の使い方

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

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

class Program
{
static async Task Main()
{
int result = await Task.Run(() =>
{
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
return sum;
});

Console.WriteLine(result);
}
}

Task<int> のように型を指定すると、非同期処理の結果を安全に受け取れます。

4-4. 複数のTaskを並列実行する方法

複数の処理を同時に開始したい場合は、タスクを複数作成してからまとめて待ちます。

C#
Task task1 = Task.Run(() => Console.WriteLine("処理1"));
Task task2 = Task.Run(() => Console.WriteLine("処理2"));
Task task3 = Task.Run(() => Console.WriteLine("処理3"));

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

ポイントは、すぐに await せず、先に複数のタスクを開始することです。

C#
// これは逐次実行に近くなる
await Task.Run(() => Work1());
await Task.Run(() => Work2());
await Task.Run(() => Work3());

並列にしたいなら、タスクを作成してから WhenAll で待ちます。

4-5. Task.WhenAllとTask.WhenAnyの使い分け

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

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

Task.WhenAny は、いずれか1つのタスクが完了した時点で先に進みます。

C#
Task completed = await Task.WhenAny(task1, task2, task3);

すべての結果が必要なら WhenAll、最初に終わった結果だけを使いたいなら WhenAny が向いています。Microsoftのドキュメントでも、Task.WhenAll は複数タスクの完了待ち、Task.WhenAny はいずれかのタスク完了待ちに使うメソッドとして説明されています。Microsoft Learn+1

5. async/awaitで非同期処理を書く方法

5-1. async/awaitの基本構文

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

C#
static async Task Main()
{
await DoAsync();
}

static async Task DoAsync()
{
await Task.Delay(1000);
Console.WriteLine("1秒後に実行");
}

戻り値がない非同期メソッドは Task、戻り値がある場合は Task<T> を返します。

C#
static async Task<int> GetNumberAsync()
{
await Task.Delay(1000);
return 123;
}

5-2. awaitで処理を待ってもUIが固まらない理由

await は、処理の完了を待つ間に呼び出し元へ制御を戻します。そのため、UIスレッドで await しても、待機中にメッセージ処理が続き、画面が固まりにくくなります。

たとえば、Windows FormsやWPFのボタンクリックで次のように書けます。

C#
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;

await Task.Delay(3000);

button1.Enabled = true;
}

このコードでは3秒待ちますが、Thread.Sleep(3000) と違ってUIスレッドを完全に停止させません。

5-3. async voidを避けるべき理由

async void は、基本的にイベントハンドラー以外では避けるべきです。

C#
// 避けたい例
async void DoAsync()
{
await Task.Delay(1000);
}

async void は呼び出し側が完了を待てず、例外も扱いにくくなります。通常は Task を返します。

C#
// 推奨
async Task DoAsync()
{
await Task.Delay(1000);
}

ただし、Windows FormsやWPFのイベントハンドラーは戻り値を void にする必要があるため、次のような async void は一般的です。

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

5-4. Task.Runとasync/awaitを組み合わせる例

CPU負荷の高い処理をUIスレッドから逃がす場合は、Task.Runawait を組み合わせます。

C#
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;

int result = await Task.Run(() =>
{
int total = 0;
for (int i = 0; i < 100_000_000; i++)
{
total += i;
}
return total;
});

label1.Text = result.ToString();
button1.Enabled = true;
}

重い計算は別スレッドで実行し、await の後はUIスレッドに戻ってラベルを更新できます。

5-5. I/O処理とCPU負荷の高い処理での使い分け

I/O処理とCPU処理では、考え方が異なります。

ファイル読み込み、HTTP通信、DBアクセスなどのI/O処理では、提供されている非同期APIをそのまま await するのが基本です。

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

一方、画像処理、大量計算、データ変換などCPU負荷の高い処理では、Task.Run で別スレッドに逃がすことを検討します。

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

Microsoftの非同期シナリオ解説でも、I/OバウンドとCPUバウンドでは非同期コードの実装方法が異なると説明されています。Microsoft Learn

6. Thread・Task・async/awaitの違いと使い分け

6-1. Threadが向いているケース

Thread が向いているのは、スレッドを明示的に管理したいケースです。

たとえば、長時間動作する専用スレッドを作りたい場合、スレッドの名前や状態を細かく管理したい場合、スレッドのライフサイクルを自分で制御したい場合です。

C#
Thread thread = new Thread(() =>
{
while (true)
{
// 常駐処理
Thread.Sleep(1000);
}
});

thread.IsBackground = true;
thread.Start();

ただし、通常のアプリ開発で最初に選ぶべき方法ではありません。Thread は低レベルで、例外、戻り値、キャンセル、複数処理の合成を自分で扱う必要があります。

6-2. Taskが向いているケース

Task は、非同期処理や並列処理の結果を表す抽象化です。戻り値、例外、キャンセル、複数タスクの待機を扱いやすいため、多くのケースで Thread より適しています。

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

CPU負荷の高い処理をバックグラウンドで実行したい場合や、複数の処理を並列に実行したい場合は Task が向いています。

6-3. async/awaitが向いているケース

async/await は、非同期処理を直感的に書きたい場合に向いています。

C#
var data = await GetDataAsync();
Console.WriteLine(data);

コールバックを多用せず、上から下に読めるコードにできるため、保守性が高くなります。ファイル、ネットワーク、DBなどのI/O処理では、基本的に async/await を使いましょう。

6-4. UIアプリでおすすめの使い分け

Windows FormsやWPFなどのUIアプリでは、次の使い分けがおすすめです。

  • API通信やファイル読み込みは async/await

  • 重い計算は Task.Run + await

  • UI更新はUIスレッドで行う

  • Thread.SleepTask.Wait().Result はUIスレッドで使わない

UIアプリで重要なのは、UIスレッドを止めないことです。時間のかかる処理は非同期化し、処理完了後にUIスレッドへ戻して画面を更新します。

6-5. サーバーサイドでおすすめの使い分け

ASP.NET Coreなどのサーバーサイドでは、I/O処理を async/await で書くのが基本です。

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

サーバーではリクエストを同時に大量処理するため、待機中にスレッドを占有しないことが重要です。I/O処理に対してむやみに Task.Run を使うと、スレッドプールを余計に消費する場合があります。

6-6. 初心者が基本的にTaskとasync/awaitを選ぶべき理由

初心者がC#で別スレッドや非同期処理を書くなら、まず Taskasync/await を選ぶのがおすすめです。

理由は、戻り値を受け取りやすく、例外処理を書きやすく、複数処理の待機が簡単で、UIアプリやサーバーサイドでも使いやすいからです。

Thread は仕組みを理解するためには大切ですが、実務で多く使うのは Taskasync/await です。

7. UIアプリで別スレッドを使うときの注意点

7-1. UIスレッドとは何か

UIスレッドとは、画面の描画、ユーザー入力、イベント処理などを担当するスレッドです。Windows FormsやWPFでは、基本的にUIコントロールは作成されたUIスレッドから操作する必要があります。

WPFではUIスレッドが入力、イベント、描画、アプリケーションコードを処理し、Dispatcher が作業項目を管理します。MicrosoftのWPFスレッドモデル解説でも、UIスレッドとDispatcherの関係が説明されています。Microsoft Learn

7-2. 別スレッドからUIを直接更新してはいけない理由

別スレッドからUIコントロールを直接更新すると、クロススレッド操作エラーや不安定な動作の原因になります。

悪い例は次の通りです。

C#
Task.Run(() =>
{
label1.Text = "完了"; // UIスレッド以外から更新しているため危険
});

UIを更新する場合は、Windows Formsなら Invoke、WPFなら Dispatcher を使ってUIスレッドに処理を戻します。

7-3. Windows FormsでInvokeを使う例

Windows Formsでは、別スレッドからUIを更新する場合に Invoke を使います。

C#
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
string result = "処理完了";

this.Invoke(() =>
{
label1.Text = result;
});
});
}

.NET 9以降のWindows Formsでは Control.InvokeAsync も利用できますが、基本的な考え方は「UI更新はUIスレッドに戻して行う」です。

7-4. WPFでDispatcherを使う例

WPFでは、Dispatcher を使ってUIスレッドに戻します。

C#
private void Button_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
string result = "処理完了";

Dispatcher.Invoke(() =>
{
LabelResult.Content = result;
});
});
}

async/await を使う場合は、await 後にUIスレッドへ戻ることが多いため、次のように書けます。

C#
private async void Button_Click(object sender, RoutedEventArgs e)
{
string result = await Task.Run(() =>
{
return "処理完了";
});

LabelResult.Content = result;
}

7-5. UIが固まる原因と解決方法

UIが固まる主な原因は、UIスレッドで時間のかかる処理を実行していることです。

悪い例です。

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

改善例です。

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

CPU負荷の高い処理なら、次のようにします。

C#
private async void button1_Click(object sender, EventArgs e)
{
int result = await Task.Run(() => HeavyCalculation());
label1.Text = result.ToString();
}

UIアプリでは「待ち時間は await」「重い計算は Task.Run」「UI更新はUIスレッド」が基本です。

8. 別スレッド処理を安全に書くためのポイント

8-1. 共有変数を扱うときの危険性

複数のスレッドから同じ変数を読み書きすると、競合状態が起きることがあります。

C#
int count = 0;

Parallel.For(0, 10000, _ =>
{
count++;
});

Console.WriteLine(count);

count++ は一見1つの処理に見えますが、実際には読み取り、加算、書き込みが組み合わさっています。複数スレッドで同時に実行されると、期待した値にならないことがあります。

8-2. lockを使った排他制御

共有データを安全に更新するには、lock を使って同時実行を制限します。

C#
object gate = new object();
int count = 0;

Parallel.For(0, 10000, _ =>
{
lock (gate)
{
count++;
}
});

Console.WriteLine(count);

lock は、共有リソースへ同時にアクセスされることを防ぐための基本的な構文です。MicrosoftのC#リファレンスでも、lock は共有リソースへのアクセスを同期するためのステートメントとして説明されています。Microsoft Learn

8-3. ConcurrentQueueやConcurrentDictionaryの活用

複数スレッドからコレクションを操作する場合は、List<T>Dictionary<TKey,TValue> をそのまま共有するより、スレッドセーフなコレクションを使う方が安全です。

C#
using System.Collections.Concurrent;

ConcurrentQueue<int> queue = new ConcurrentQueue<int>();

queue.Enqueue(1);
queue.Enqueue(2);

if (queue.TryDequeue(out int value))
{
Console.WriteLine(value);
}

辞書であれば ConcurrentDictionary<TKey,TValue> が使えます。

C#
var dictionary = new ConcurrentDictionary<string, int>();

dictionary.TryAdd("apple", 1);
dictionary.AddOrUpdate("apple", 1, (_, oldValue) => oldValue + 1);

ConcurrentQueue<T>ConcurrentDictionary<TKey,TValue> は、並行処理で使いやすいコレクションとして用意されています。Microsoft Learn+1

8-4. デッドロックが起きる原因

デッドロックとは、複数の処理がお互いの完了を待ち続け、先に進めなくなる状態です。

たとえば、UIスレッドで .ResultWait() を使って非同期処理を待つと、非同期処理の続きがUIスレッドに戻れず、停止することがあります。

C#
// UIスレッドでは避ける
string result = GetDataAsync().Result;

改善例です。

C#
string result = await GetDataAsync();

また、複数の lock を異なる順序で取得することもデッドロックの原因になります。排他制御は範囲を小さくし、ロックの順序を統一しましょう。

8-5. スレッドセーフな設計にするコツ

スレッドセーフな設計にするには、次の考え方が重要です。

  • 共有データを減らす

  • 可能なら不変オブジェクトを使う

  • 共有する場合は lock やスレッドセーフコレクションを使う

  • UI更新はUIスレッドに限定する

  • キャンセルと例外処理を設計に含める

  • .ResultWait() に頼らず await する

別スレッド処理は、コードが動いているように見えても、タイミングによってバグが出ることがあります。再現しづらい不具合を防ぐためにも、最初から安全な設計にすることが大切です。

9. 別スレッド処理のキャンセル・例外処理・終了制御

9-1. CancellationTokenで処理をキャンセルする方法

C#でタスクを安全にキャンセルするには、CancellationToken を使います。

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

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

Task task = Task.Run(() => Work(cts.Token));

await Task.Delay(2000);
cts.Cancel();

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

static void Work(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();

Console.WriteLine("処理中...");
Thread.Sleep(500);
}
}
}

CancellationToken は強制終了ではなく、処理側がキャンセル要求を確認して自分で終了する協調的キャンセルです。Microsoftのドキュメントでも、キャンセル通知には CancellationTokenSource.Cancel を使い、不要になった CancellationTokenSource は破棄することが説明されています。Microsoft Learn

9-2. Task内の例外をtry-catchで受け取る方法

Task 内で発生した例外は、await することで呼び出し元で受け取れます。

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

Task を開始しただけで放置すると、例外に気づきにくくなります。基本的に、開始したタスクは await して、例外を処理しましょう。

9-3. Thread.Abortを使ってはいけない理由

Thread.Abort でスレッドを強制終了する方法は使うべきではありません。現在の.NETでは Thread.Abort は非推奨・未サポートで、PlatformNotSupportedException をスローするものとして説明されています。Microsoftの互換性情報でも、Thread.Abort ではなく CancellationToken を使うことが推奨されています。Microsoft Learn+1

悪い例です。

C#
// 使わない
thread.Abort();

代わりに、終了フラグや CancellationToken を使って安全に止めます。

C#
CancellationTokenSource cts = new CancellationTokenSource();
cts.Cancel();

9-4. タイムアウトを実装する方法

一定時間で処理を打ち切りたい場合は、CancellationTokenSource.CancelAfter を使えます。

C#
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));

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

Task.WhenAny を使ってタイムアウトを表現する方法もあります。

C#
Task work = LongRunningAsync();
Task timeout = Task.Delay(5000);

if (await Task.WhenAny(work, timeout) == timeout)
{
Console.WriteLine("タイムアウト");
}
else
{
await work;
}

キャンセル可能な処理なら、CancellationToken を渡す方法の方が安全です。

9-5. アプリ終了時にスレッドを安全に止める方法

アプリ終了時には、別スレッドやバックグラウンド処理を放置しないようにします。

基本方針は次の通りです。

  • CancellationTokenSource.Cancel() で終了要求を出す

  • タスクが完了するまで await する

  • ファイル、DB接続、ネットワーク接続などを Dispose する

  • Thread.Abort に頼らない

  • 無限ループには必ず終了条件を入れる

例です。

C#
private CancellationTokenSource? _cts;
private Task? _task;

private void Start()
{
_cts = new CancellationTokenSource();
_task = Task.Run(() => Work(_cts.Token));
}

private async Task StopAsync()
{
if (_cts == null || _task == null)
{
return;
}

_cts.Cancel();

try
{
await _task;
}
catch (OperationCanceledException)
{
// 正常なキャンセル
}
finally
{
_cts.Dispose();
_cts = null;
_task = null;
}
}

10. C#の別スレッド実装でよくあるエラーと対処法

10-1. クロススレッド操作エラーの原因

クロススレッド操作エラーは、UIスレッド以外からUIコントロールを直接操作したときに発生します。

悪い例です。

C#
Task.Run(() =>
{
textBox1.Text = "更新"; // 危険
});

Windows Formsでは Invoke、WPFでは Dispatcher を使ってUIスレッドに戻しましょう。

C#
this.Invoke(() =>
{
textBox1.Text = "更新";
});

10-2. UIがフリーズする原因

UIがフリーズする原因は、UIスレッドで重い処理やブロッキング処理をしていることです。

よくある原因は次の通りです。

  • Thread.Sleep をUIスレッドで使っている

  • Task.Wait() をUIスレッドで使っている

  • .Result で非同期処理を同期的に待っている

  • 大量ループや画像処理をUIスレッドで実行している

対処法は、await で非同期に待つこと、CPU処理は Task.Run に逃がすことです。

10-3. awaitしているのに別スレッドにならない原因

await は「別スレッドで実行する命令」ではありません。await は、タスクの完了を非同期的に待つ構文です。

たとえば次のコードは、必ずしも別スレッドで重い処理をするわけではありません。

C#
await SomeAsync();

CPU負荷の高い同期処理を別スレッドで実行したいなら、Task.Run を使います。

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

一方、HTTP通信やファイル読み込みのようなI/O処理では、非同期APIをそのまま await します。

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

10-4. Task.Runを使いすぎる問題

Task.Run は便利ですが、何でも包めばよいわけではありません。

特にサーバーサイドでI/O処理を Task.Run で包むと、スレッドプールを無駄に使うことがあります。

避けたい例です。

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

改善例です。

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

Task.Run はCPU負荷の高い同期処理を別スレッドに逃がすために使い、I/O処理は非同期APIを直接 await するのが基本です。

10-5. デバッグ時に確認すべきポイント

別スレッド処理のデバッグでは、次の点を確認しましょう。

  • どのスレッドで実行されているか

  • UIスレッドをブロックしていないか

  • await し忘れているタスクがないか

  • 例外が握りつぶされていないか

  • 共有変数を複数スレッドから更新していないか

  • キャンセル時にリソースが解放されているか

スレッドIDを出力すると、処理がどのスレッドで動いているか確認しやすくなります。

C#
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

11. 用途別:C#で別スレッドを使う実装パターン

11-1. 重い計算処理を別スレッドで実行する例

CPU負荷の高い処理は Task.Run で実行します。

C#
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;

int result = await Task.Run(() =>
{
int total = 0;

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

return total;
});

label1.Text = result.ToString();
button1.Enabled = true;
}

この形にすると、計算中もUIスレッドをブロックしにくくなります。

11-2. ファイル読み込みを非同期で行う例

ファイル読み込みには、非同期APIを使います。

C#
private async Task<string> LoadTextAsync(string path)
{
return await File.ReadAllTextAsync(path);
}

UIアプリで使う場合です。

C#
private async void button1_Click(object sender, EventArgs e)
{
string text = await File.ReadAllTextAsync("sample.txt");
textBox1.Text = text;
}

ファイルI/OはCPU処理ではないため、基本的に Task.Run で包む必要はありません。

11-3. API通信をasync/awaitで行う例

API通信では HttpClientasync/await を使います。

C#
using System.Net.Http;

private static readonly HttpClient _httpClient = new HttpClient();

private async Task<string> GetApiAsync()
{
string url = "https://example.com/api/data";
return await _httpClient.GetStringAsync(url);
}

呼び出し側です。

C#
private async void button1_Click(object sender, EventArgs e)
{
string json = await GetApiAsync();
textBox1.Text = json;
}

通信完了までUIをブロックせずに待てます。

11-4. 定期処理を別スレッドで動かす例

定期処理は、CancellationTokenTask.Delay を組み合わせます。

C#
private CancellationTokenSource? _cts;

private void StartTimerLoop()
{
_cts = new CancellationTokenSource();
_ = RunLoopAsync(_cts.Token);
}

private void StopTimerLoop()
{
_cts?.Cancel();
}

private async Task RunLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Console.WriteLine("定期処理");

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

while (true)Thread.Sleep だけで書くより、キャンセルしやすく安全です。

11-5. 進捗表示付きでバックグラウンド処理を行う例

進捗表示には IProgress<T>Progress<T> が便利です。

C#
private async void button1_Click(object sender, EventArgs e)
{
progressBar1.Value = 0;

var progress = new Progress<int>(value =>
{
progressBar1.Value = value;
});

await Task.Run(() => Work(progress));
}

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

Progress<T> を使うと、UI側で安全に進捗を受け取りやすくなります。

12. C#で別スレッドを使うときのベストプラクティス

12-1. 基本はTaskとasync/awaitを使う

C#で別スレッドや非同期処理を書くときは、基本的に Taskasync/await を使いましょう。

Thread は低レベルで自由度が高い一方、終了制御、例外処理、戻り値、キャンセル、同期処理を自分で設計する必要があります。通常のアプリ開発では Task の方が安全で扱いやすいです。

12-2. UI更新は必ずUIスレッドに戻す

UIアプリでは、別スレッドからUIコントロールを直接操作してはいけません。

Windows Formsでは Invoke、WPFでは Dispatcher を使います。async/await を使う場合は、await 後にUIスレッドへ戻る流れを利用できることも多いです。

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

12-3. 共有データは最小限にする

別スレッド処理で最もバグになりやすいのが共有データです。

複数スレッドから同じ変数やコレクションを更新すると、競合状態が起きます。共有する必要がある場合は、lockConcurrentQueueConcurrentDictionary などを使いましょう。

ただし、最もよい設計は「そもそも共有しない」ことです。引数と戻り値でデータを受け渡し、状態を局所化すると安全性が上がります。

12-4. キャンセルと例外処理を必ず用意する

別スレッド処理は、開始するだけでなく、止め方と失敗時の扱いまで考える必要があります。

C#
try
{
await DoWorkAsync(token);
}
catch (OperationCanceledException)
{
// キャンセル
}
catch (Exception ex)
{
// エラー処理
}

長時間処理には CancellationToken を渡し、タスクは await して例外を受け取るようにしましょう。

12-5. 可読性と保守性を重視した実装にする

別スレッド処理は複雑になりやすいため、可読性を重視することが大切です。

避けたい書き方は、ネストが深いコード、キャンセルできない無限ループ、Task.Run の乱用、例外を無視するコード、共有変数をあちこちから更新するコードです。

おすすめは、処理を小さなメソッドに分けることです。

C#
private async Task ExecuteAsync(CancellationToken token)
{
var data = await LoadAsync(token);
var result = await Task.Run(() => Calculate(data), token);
await SaveAsync(result, token);
}

このように、I/O処理は非同期API、CPU処理は必要に応じて Task.Run、キャンセルは CancellationToken、例外は try-catch で扱うと、保守しやすいコードになります。

まとめ

C#で別スレッドを使う方法には、ThreadTaskasync/awaitThreadPoolBackgroundWorker などがあります。

Thread はスレッドを直接作成・制御する低レベルな方法です。スレッドの寿命を細かく管理したい場合には使えますが、通常のアプリ開発では扱いが難しくなりやすいです。

Task は、別スレッド処理や非同期処理を扱いやすくするための仕組みです。Task.Run を使えば、CPU負荷の高い処理をバックグラウンドで実行できます。戻り値は Task<T>、複数処理の待機は Task.WhenAllTask.WhenAny で扱えます。

async/await は、非同期処理を読みやすく書くための構文です。ただし、await したからといって必ず別スレッドになるわけではありません。I/O処理では非同期APIをそのまま await し、CPU負荷の高い処理では必要に応じて Task.Run を使うのが基本です。

UIアプリでは、UIスレッドをブロックしないことが重要です。重い処理は別スレッドへ逃がし、UI更新は必ずUIスレッドに戻して行います。

また、別スレッド処理では、共有データ、デッドロック、キャンセル、例外処理にも注意が必要です。Thread.Abort のような強制終了ではなく、CancellationToken を使った協調的キャンセルを設計しましょう。

初心者はまず、Taskasync/await を中心に覚えるのがおすすめです。そのうえで、必要に応じて ThreadThreadPool、排他制御、スレッドセーフコレクションを理解していくと、C#の別スレッド処理を安全に実装できるようになります。