1. はじめに
プロセスをスーパーマーケットに例えると、スレッド(thread)はそれぞれ別のレジで働く店員のようなものです。全員が同じ店で働いていますが、各自が並行して顧客をさばくので、作業が速く効率的になります。プロセス内のスレッドを使うと、共通リソースを共有しながら複数のタスクを同時に実行できます。重要な利点は、プロセッサがマルチタスクをサポートしていればスレッドは本当に並列で動ける点です。
実践でスレッドが必要になるケース
なぜ複数のスレッドを起動する必要があるのか? 実際の状況をいくつか挙げると:
- GUI アプリを書いていて、重い処理中にアプリが「フリーズ」するのを避けたいとき。
- 複数のファイルを同時にダウンロードする必要があるとき。
- ゲームで敵キャラが他と独立して「考える」必要があるとき。
驚くことに、多くのプログラムで未だにスレッドの扱いが未熟で「フリーズ」が発生します。今日はそうした問題を避ける方法を学びます。
クラス Thread: 手動マルチスレッドの基礎
クラス Thread は .NET のマルチスレッドプログラミングの中では古典的な存在です。よりモダンなツール(Task、async/await)が出てきても、スレッドを一から扱ってみたいときには Thread を使う価値があります。
スレッド作成の流れ
- スレッドで実行するメソッドを渡して Thread オブジェクトを作る。
- Start() を呼んでスレッドを開始する。
- (任意)何が起きているか確認する — すべてが並行して動くかもしれません!
2. Thread を使ったスレッドの起動
実際に並列処理の魔法を体験してみましょう。簡単なクラスを追加して、ある数まで数えて進捗を出力する処理を別スレッドで動かします。
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("メインスレッドがスタートしました!");
// 実行するメソッドを指定してスレッドオブジェクトを作成
Thread workerThread = new Thread(CountToTen);
// 新しいスレッドを開始
workerThread.Start();
// メインスレッドも何かしている: ドットを出力...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // 見やすくするための遅延
}
Console.WriteLine("\nメインスレッド終了!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[スレッド] カウント: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[スレッド] 完了!");
}
}
何が起きているか?
コンソールでは、ドットと "カウント: X" が入り交じって表示されます。これがマルチスレッドの最初のサインです! メインスレッドは自分のドットを出力し、新しいスレッドは 10 まで数えます。互いに邪魔し合わず、バンドの二人の演奏のように別々に動いて全体として機能しています。
3. スレッドにデータを渡すには?
スレッドが何をするかだけでなく、何で処理するかも知る必要がある場合があります。引数を取るメソッドをスレッドで呼ぶにはどうするか?
方法1: ラムダ(匿名メソッド)を使う
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[スレッド] {i} / {n}");
Thread.Sleep(300);
}
}
ここでは必要なメソッド呼び出しをラムダでラップして引数を渡しています。これはよく使われる手法で、Thread が引数無しのメソッド(ThreadStart)を期待しているためです。
方法2: ParameterizedThreadStart を使う
1 つの object 型のパラメータを受け取るデリゲート ParameterizedThreadStart を使うこともできます。
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[スレッド] {i} / {max}");
Thread.Sleep(200);
}
}
パラメータは object 型なのでキャストが必要です。モダンな C# ではラムダの方が好まれることが多いですが、この方法も動作します。
4. スレッドのライフサイクル管理
クラス Thread が提供する便利な機能を見ていきましょう。
| プロパティ / メソッド | 用途 |
|---|---|
|
スレッドを開始する(作成時に指定したメソッドを実行) |
|
スレッドの終了を待つ(呼び出し元スレッドをブロックして、対象スレッドの終了を待機) |
|
スレッドが現在動作中かを示す(true/false) |
|
スレッドに名前を付ける(デバッグ時に便利) |
|
現在のスレッドを表すオブジェクトを取得する |
|
現在のスレッドを ms ミリ秒だけ停止する |
例: スレッドの終了を待つ
時にはメインスレッドが補助スレッドの終了を待つ必要があります。
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[セカンドスレッド] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[メインスレッド] セカンドスレッドの終了を待ちます...");
t.Join(); // メインスレッドはここで待機
Console.WriteLine("[メインスレッド] セカンドスレッドが終了しました!");
Join() がないと、プログラムはスレッドが動いている最中に終了してしまうことがあります。Join() を使えばメインスレッドは全てが終わるまで待ちます。
5. 便利なポイント
スレッドの命名: 混乱を避けるために
デバッグのためにスレッドに名前を付けると便利です:
Thread t = new Thread(() =>
{
Console.WriteLine($"これは実行中のスレッドです: {Thread.CurrentThread.Name}");
});
t.Name = "カウンタースレッド";
t.Start();
スレッドが増えてそれぞれ別の仕事をしているときに役立ちます。
制約と現実的な将来
まず言っておくと、現代のアプリでは手動で Thread を管理することは稀です。実際にはより賢いツール(Task、async/await)を使うことが多く、これらは後で詳しく扱います。しかしスレッドの基本を理解することは以下の点で重要です:
- C# と .NET の「舞台裏」を理解するため。
- 面接で Thread と Task の違いを説明するよう求められることがあるため。
- 大きな遺産コードの問題診断やトラブルシューティングのため。
総括図: スレッドのライフサイクル
stateDiagram-v2
[*] --> New: Thread 作成
New --> Running: Start()
Running --> Stopped: メソッド完了
Stopped --> [*]
これで C# でスレッドを自分で作成・起動する方法を学びました。あなたはもう列車の乗客ではなく、複数の車両を同時に動かす運転手です! 次はスレッドのライフサイクル、管理、同期、そして並列処理の新たな地平線です。
6. Thread を使う際の典型的なミスとコツ
ミス №1: スレッドを開始しないこと。
Thread オブジェクトを作成したのに Start() を呼び忘れることがよくあります。結果としてスレッドが始まらず、原因の特定が難しくなります。
ミス №2: 同期なしで共有データを変更すること。
複数のスレッドが同じ変数を保護なしで操作すると問題になります。これは二人の店員が同じ小銭箱からお釣りを出しているようなもので、すぐに混乱や不具合が起きます。
ミス №3: 廃止された危険なメソッドの使用。
Thread.Suspend()、Thread.Resume() などのメソッドは危険で廃止されています。スレッドのライフサイクルは別の方法で管理してください。
ミス №4: スレッド内での未処理例外。
スレッド内で例外が発生してキャッチされないと、そのスレッドは終了しますがメインスレッドは気づかないことがあります。スレッドのコードは try-catch で囲んでエラーをログに残すようにしましょう。
Thread t = new Thread(() =>
{
try
{
// ... あなたのコード
}
catch (Exception ex)
{
// エラーをログ出力
Console.WriteLine($"[スレッド] エラー: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION