CodeGym /コース /C# SELF /スレッドのライフサイクルとその管理

スレッドのライフサイクルとその管理

C# SELF
レベル 55 , レッスン 2
使用可能

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("ワーカースレッドが仕事を終えた!");
    }
}

プログラムは何を出力するか?

  1. スレッド作成後 — ステータスは Unstarted になる。
  2. Start後 — 通常はすぐに Running(ただし場合によっては Running | Background のようになることも)。
  3. 実行中 — ステータスは Running、あるいはスレッドが「寝ている」なら WaitSleepJoin になり得る。
  4. メソッド終了後 — ステータスは 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
new Thread(...)
スレッドが作られ、起動していない Start() を呼ぶ
Running
Start()
スレッドが仕事をしている メソッドを終了する
WaitSleepJoin Sleep(), Join(), 待機 スレッドが一時的に非アクティブ 待ちが終わる
Stopped スレッドメソッドが終了した スレッドは「死んでいる」 戻れない — 終わり

スレッドライフ管理に関するFAQ

質問: コマンドでスレッドを殺せる?
答え: いいえ、そして不要。スレッドは自分で終了を管理すべき。キャンセルフラグを使おう。

質問: Threadオブジェクトを再利用できる?
答え: できない。新しい仕事には新しいオブジェクトを作る。

質問: メインスレッドが終わって子スレッドだけ残ったらどうなる?
答え: 子スレッドがバックグラウンド(IsBackground == true)ならアプリは終了する。そうでなければ、全スレッドが終わるまでプロセスは生き続ける。

質問: キャンセルでスレッドが終わるときにリソースをどう正しく解放する?
答え: スレッドメソッド内で try...finally ブロックを使って、必ずリソースを解放するようにする。

8. スレッド操作でよくあるミスとその回避法

ミスその1: 同じ Thread オブジェクトを再利用しようとする。
同じスレッドオブジェクトを2回以上起動してはいけない。スレッドが終わったら再起動は不可で、例外になる。

ミスその2: スレッド内で外部リソースを正しく解放しない。
スレッドがファイルやネットワークなどを扱う場合は、必ずクローズや解放を行う。finally ブロックや using を使うのが推奨。

ミスその3: あまりに多くのスレッドを作る。
過剰なスレッド数はデバッグを難しくし、パフォーマンスを悪化させる。余計なスレッドはバグの温床になることが多い。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION