HOWTO · Csharp
C# でスレッドを停止する方法
この記事では、C# で Thread を開始して停止する方法を説明します。
このページの内容
C# アプリケーションで Thread によって作成したワーカーを停止する必要がある場合、旧式の Abort() メソッドを使ってランタイムに終了を要求できます。この手法は Thread.Abort() をサポートするランタイムでのみ利用できます。最新の .NET ランタイムでは、代わりに PlatformNotSupportedException がスローされることがあります。新しいコードでは、通常、CancellationToken を使った協調的キャンセルのほうが安全です。ワーカー自身がリソースを解放し、共有状態を既知の状態にしてから終了できるためです。
Abort() をサポートするランタイムでは、Abort() によって対象スレッドで ThreadAbortException が発生します。この例外は非同期的に発生するため、ワーカーがロックを保持している間や共有データを更新している間に中断される可能性があります。この例は互換性を確認するためのものと考え、対象ランタイムが API を明示的にサポートしている場合に限って使用してください。
C# で Abort() メソッドを使用してスレッドを停止する
Thread.Abort() のドキュメントでは、この旧式の操作をサポートするランタイムについて説明されています。System.Threading 名前空間には、Thread 型とその Start() および Abort() メソッドが含まれています。以下の完全な例では、ThreadDemo.threadAssign() を定義し、Main() を持つ ThreadExampleMain クラスを作成して、ワーカーを開始し、停止を要求します。
ループは 4 回ではなく 6 回の反復に設定されています。Thread is abort という行は abort 要求の直前に出力されるため、これは診断メッセージにすぎず、ワーカーが停止したことを証明するものではありません。
次の自己完結型ソースでは、ワーカーを作成して開始し、停止を要求した後、ワーカーの終了を待機します。
using System;
using System.Threading;
public class ThreadDemo {
public void threadAssign() {
for (int threadCount = 0; threadCount < 6; threadCount++) {
Console.WriteLine("First Thread");
Thread.Sleep(10);
}
}
}
public class ThreadExampleMain {
public static void Main() {
ThreadDemo threadDemo = new ThreadDemo();
Thread thread = new Thread(new ThreadStart(threadDemo.threadAssign));
thread.Start();
Console.WriteLine("Thread is abort");
try {
thread.Abort();
}
catch (PlatformNotSupportedException) {
Console.WriteLine("Abort() is not supported by this runtime.");
}
thread.Join();
}
}
Thread is abort
First Thread
提供された実行証拠には、このソースの実行が完了したことが記録されています。Thread is abort が abort 要求より前に出力されることも確認されています。ただし、出力の順序と First Thread の行数はスケジューリングに依存するため、固定された出力を想定しないでください。実際に利用する前に、対象とするランタイムで実行してください。
Abort() をサポートするランタイムでは、ワーカーが非同期の ThreadAbortException を受け取り、停止します。これは、Main() の Abort() が同期的にスローする例外とは別のものです。要求前に出力される First Thread の行の順序と数はスレッドのスケジューリングによって変わりますが、ループ自体は 6 回の反復を許可しています。Abort() が PlatformNotSupportedException をスローした場合、ワーカーは処理を続ける可能性があり、catch 分岐はその同期的な失敗を報告します。このメッセージは診断情報であり、あらゆるコンテキストでランタイムがサポートしていないことの証明ではありません。いずれの場合も、Thread is abort の行を終了の証拠として扱わないでください。どちらの経路でも、thread.Join() は Main() が戻る前にワーカーの終了を待機します。abort がサポートされていない場合、これはワーカーが通常どおり完了するまで待機することを意味します。
CancellationToken を使ってスレッドを協調的に停止する
新しいコードでは、スレッドを強制的に止めるのではなく、協調的に停止してください。スレッドの所有者は CancellationTokenSource を作成し、その Token をワーカーに渡します。ワーカーは作業単位の間の安全な地点でトークンを確認し、キャンセル要求を見たら、新しい作業を開始せず、自分が所有するリソースを解放して戻ります。所有者は Cancel() で停止を要求し、その後 Join() を呼び出してワーカーが実際に終了するまで待ちます。キャンセルは要求にすぎません。トークンを決して確認しないワーカーや、1 回の長いブロッキング呼び出しで止まっているワーカーは、トークンに気付くまで停止しません。そのため、各ステップは短く保つか、トークンを直接受け取る API を使用してください。
token.ThrowIfCancellationRequested() を呼び出すと、キャンセルが要求されている場合に OperationCanceledException がスローされ、スレッドが戻る前に catch または finally ブロックでクリーンアップを実行できます。
using System;
using System.Threading;
public class CooperativeStop {
public static void Main() {
using CancellationTokenSource cts = new CancellationTokenSource();
Thread worker = new Thread(() => DoWork(cts.Token));
worker.Start();
Thread.Sleep(55);
cts.Cancel();
worker.Join();
Console.WriteLine("Main: worker has stopped.");
}
private static void DoWork(CancellationToken token) {
int completed = 0;
try {
while (true) {
token.ThrowIfCancellationRequested();
completed++;
Thread.Sleep(10);
}
}
catch (OperationCanceledException) {
Console.WriteLine($"Worker: cancellation noticed after {completed} steps; resources released.");
}
}
}
Worker: cancellation noticed after 6 steps; resources released.
Main: worker has stopped.
この出力は .NET 10 で記録されたものです。正確なステップ数はタイミングに依存しますが、順序は依存しません。ワーカーが先にクリーンアップを報告し、Main: worker has stopped. の行は Join() が戻った後にしか表示されないため、停止は推測ではなく確認されたことになります。
スレッドを停止するときのよくある間違い
- 新しいコードや最新のランタイムで
Abort()を使うこと。Thread.Abort()は .NET Framework でのみサポートされています。.NET Core および .NET 5 以降ではPlatformNotSupportedExceptionがスローされ(コンパイラも SYSLIB0006 を警告します)、ワーカーはまったく停止せず、最初の例が .NET 10 で示すように最後まで実行されます。最新のランタイムで動く必要があるコードには、機能するAbort()の代替はなく、代わりに協調的キャンセルが必要です。 - キャンセルを要求しながらトークンを確認しないこと。
Cancel()はフラグを設定するだけです。ワーカーのループがThrowIfCancellationRequested()を決して呼ばず、IsCancellationRequestedも読み取らなければ、何も起こらずスレッドは実行され続けます。 - 停止要求をスレッド終了の証拠として扱うこと。
Cancel()もAbort()もワーカーを待ちません。結果を使用したり、ワーカーがまだ触れているオブジェクトを解放したりする前に、必ずJoin()を呼んでください(または別の方法でワーカーの完了を確認してください)。 - 更新の途中でワーカーを中断すること。
Abort()が機能するランタイムでは、ワーカーがロックを保持しているときや共有データを更新し終えていないときに非同期例外が到着し、他のすべてのスレッドにとって状態が不整合なままになることがあります。協調的キャンセルではワーカー自身が停止する安全な地点を選ぶため、これを回避できます。