1. はじめに
スレッドを、あなたが仕事を頼むバイタリティあふれる社員だと想像してみて。社員は寝ている(まだ仕事を始めていない)、一生懸命働いている(あなたのメソッドが実行中)、あなたから新しい仕事をもらうまで待っている(アイドル)、あるいは仕事を終えて退社する(終了)ことがある。
C#(というか.NET全般)でのスレッドのライフサイクルは複数の状態からなる:
- Unstarted — スレッドは作られたが、まだ起動していない。
- Running — スレッドが実行中。
- WaitSleepJoin — スレッドは一時的に働いていない(例えば、シグナル待ちや「睡眠」状態)。
- Stopped — スレッドがタスクを終えて終了した。
このサイクルは図でイメージできる:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: シグナル受信/時間切れ
Running --> Stopped: メソッド終了
WaitSleepJoin --> Stopped: メソッド終了
Stopped --> [*]
すべてはThreadオブジェクトの作成から始まるけど、Start()を呼ばない限り、スレッドはUnstartedで「まどろんで」いる。Start()を呼ぶと、いよいよ実行開始でRunningへ。スレッド内のコードがThread.Sleepを呼ぶか何かを待つと、待機状態に入る。渡したメソッドが終わるとスレッドは死に、一旦終了したスレッドは復活しない。片道切符だよ。
2. 実践:シンプルなスレッドのライフサイクル
古典的な例を見てみよう:
using System;
using System.Threading;
class Program
{
static void Main()
{
// スレッドを作成 — 今は仕事を予定しているだけ
Thread worker = new Thread(DoWork);
Console.WriteLine($"スレッド作成後の状態: {worker.ThreadState}");
// スレッドを起動
worker.Start();
Console.WriteLine($"起動後のスレッド状態: {worker.ThreadState}");
// メインスレッドを少し寝かせて、ワーカースレッドが作業する時間を与える
Thread.Sleep(100);
Console.WriteLine($"後で見たスレッドの状態: {worker.ThreadState}");
// workerが終わるのを待つ(結合)
worker.Join();
Console.WriteLine($"終了後のスレッド状態: {worker.ThreadState}");
Console.WriteLine("メインスレッド終了");
}
static void DoWork()
{
Console.WriteLine("ワーカースレッドが動き始めた!");
Thread.Sleep(500);
Console.WriteLine("ワーカースレッドが仕事を終えた!");
}
}
プログラムは何を出力するか?
- スレッド作成後 — ステータスは Unstarted になる。
- Start後 — 通常はすぐに Running(ただし場合によっては Running | Background のようになることも)。
- 実行中 — ステータスは Running、あるいはスレッドが「寝ている」なら WaitSleepJoin になり得る。
- メソッド終了後 — ステータスは Stopped になる。
このコードは、スレッドがどんな状態になりうるかを理解するのにうってつけ。遅延をいじって状態変化を観察してみて。
3. スレッドの管理:主なメソッド
起動: Start()
当たり前だけど繰り返すよ:スレッドを作ったら Start() で起動する。しかも1回だけ:Start() を再度呼ぶと ThreadStateException が投げられる。
Thread t = new Thread(MyMethod);
t.Start(); // OK
t.Start(); // エラー!
終了を待つ: Join()
時々、スレッドが終わるのを待ってから次に進みたいことがある。そのために Join() がある。
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // tが終わるまで現在のスレッドをブロックする
複数のスレッドがある場合は、それぞれに Join() を呼べば、メインは全員が終わるまで待つ。
バリエーション:指定時間だけ待つ Join(int millisecondsTimeout) のオーバーロードがある。
// 最大2秒待つ
if (t.Join(2000))
Console.WriteLine("スレッドは時間内に終了した");
else
Console.WriteLine("もう待つのは飽きた...");
強制終了:なぜダメなのか
古い.NETではスレッドをその場で「殺す」Thread.Abort() のようなメソッドがあった。今ではほとんど使われない—危険でプログラムを不整合な状態にすることがある。.NETの哲学はこう:スレッドは自発的に終了すべき。社員をぶった切るんじゃなくて、穏やかに「今日は終わりだよ」と伝えるべきなんだ。
4. 正しくスレッドを「止める」方法
もっとも正しく安全な方法は、キャンセルフラグや終了フラグを使い、スレッドが定期的にそれをチェックするやり方。
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("働いてるよ!");
Thread.Sleep(300);
}
Console.WriteLine("コマンドでスレッドは仕事を終える。");
}
public void RequestStop()
{
shouldStop = true;
}
}
使い方:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// 少し待つ
Thread.Sleep(1000);
// スレッドに終了を頼む
w.RequestStop();
t.Join(); // スレッドの終了を待つ
重要な点: volatile
キーワード volatile はコンパイラやCPUに「このフィールドをキャッシュするな、常に最新を読め」と伝える。これがないと(あるいは他の同期手段がないと)スレッドが終了フラグの変更に永遠に気づかない可能性がある。
5. スレッドが待機や睡眠状態になる場合
スレッドが一時的に仕事をしないことがある — 待っているか、寝ているか。
睡眠: Thread.Sleep
スレッドに休ませたいときや(例えばCPUを酷使しないように)速度を落としたいときは Thread.Sleep(milliseconds) を使う。
// スレッドを2秒スリープさせる
Thread.Sleep(2000);
スリープ中はスレッドは何も仕事をしていない。
待機 / Join
メインスレッドが子スレッドの終了を待つとき(Join)はメインが一時停止する。同様に、モニタや他の同期プリミティブでリソースの解放を待つとスレッドは待機状態に入る。
6. スレッドのフォアグラウンド/バックグラウンド管理
.NETのスレッドは大きく二種類:foreground(フォアグラウンド)とbackground(バックグラウンド)。違いはシンプル:
- プロセス内にバックグラウンドスレッドだけが残っていると、プロセスは自動的に終了する。
- メインスレッドやフォアグラウンドスレッドが残っている限り、プロセスは終了しない。
スレッドを明示的にバックグラウンドに指定できる:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // バックグラウンドに設定
t.Start();
実例 — デーモン vs 通常スレッド
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("俺はファントム(バックグラウンド)、止められないぜ!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // バックグラウンド化
t.Start();
Thread.Sleep(1200);
Console.WriteLine("メインスレッドは仕事を終える");
// Mainが終わるとプロセスが死に、永遠ループ中のスレッドも消える
Mainが終了するとプロセスが終わり、バックグラウンドスレッドは自動的に停止する。
7. 便利な注意点
してはいけないこと
- スレッドを再起動してはいけない。 Threadオブジェクトは一度だけの寿命:メソッドが終わったらスレッドは死ぬ。Start()を再度呼ぶと例外になる。
- 他人のスレッドを強制停止してはいけない。 Thread.Abort() や Thread.Suspend() のような方法は古く危険。
- スレッドの終了を無視してはいけない。 スレッドがファイルやリソースを使っているなら、終了前にちゃんと解放すること。
状態チェックとライフサイクル管理
if (t.IsAlive)
{
Console.WriteLine("スレッドはまだ生きている");
}
else
{
Console.WriteLine("スレッドは終了した");
}
IsAlive はスレッドがメソッドを実行している間は true、終了後は false になる。
.NETにおけるシンプルスレッドのライフサイクル
| 状態 | どうやってなるか | 意味 | どう抜けるか |
|---|---|---|---|
| Unstarted | |
スレッドが作られ、起動していない | Start() を呼ぶ |
| Running | |
スレッドが仕事をしている | メソッドを終了する |
| WaitSleepJoin | Sleep(), Join(), 待機 | スレッドが一時的に非アクティブ | 待ちが終わる |
| Stopped | スレッドメソッドが終了した | スレッドは「死んでいる」 | 戻れない — 終わり |
スレッドライフ管理に関するFAQ
質問: コマンドでスレッドを殺せる?
答え: いいえ、そして不要。スレッドは自分で終了を管理すべき。キャンセルフラグを使おう。
質問: Threadオブジェクトを再利用できる?
答え: できない。新しい仕事には新しいオブジェクトを作る。
質問: メインスレッドが終わって子スレッドだけ残ったらどうなる?
答え: 子スレッドがバックグラウンド(IsBackground == true)ならアプリは終了する。そうでなければ、全スレッドが終わるまでプロセスは生き続ける。
質問: キャンセルでスレッドが終わるときにリソースをどう正しく解放する?
答え: スレッドメソッド内で try...finally ブロックを使って、必ずリソースを解放するようにする。
8. スレッド操作でよくあるミスとその回避法
ミスその1: 同じ Thread オブジェクトを再利用しようとする。
同じスレッドオブジェクトを2回以上起動してはいけない。スレッドが終わったら再起動は不可で、例外になる。
ミスその2: スレッド内で外部リソースを正しく解放しない。
スレッドがファイルやネットワークなどを扱う場合は、必ずクローズや解放を行う。finally ブロックや using を使うのが推奨。
ミスその3: あまりに多くのスレッドを作る。
過剰なスレッド数はデバッグを難しくし、パフォーマンスを悪化させる。余計なスレッドはバグの温床になることが多い。
GO TO FULL VERSION