HOWTO · Csharp
如何在 C# 中停止线程
本文将解释如何在 C# 中启动和停止线程。
本页内容
当 C# 应用必须停止使用 Thread 创建的工作线程时,旧式的 Abort() 方法可以请求运行时终止该线程。此方法只适用于支持 Thread.Abort() 的运行时;现代 .NET 运行时可能会改为抛出 PlatformNotSupportedException。对于新代码,通常更安全的做法是使用通过 CancellationToken 协作取消,因为工作线程可以释放资源,并使共享状态保持在已知且一致的状态。
在支持该 API 的运行时中,Abort() 会使目标线程产生 ThreadAbortException。此异常是异步的,因此工作线程可能在持有锁或更新共享数据时被中断。请将此示例视为兼容性参考,仅在目标运行时明确支持该 API 时使用它。
在 C# 中使用 Abort() 方法停止线程
Thread.Abort() 文档介绍了此旧式操作的运行时支持情况。System.Threading 命名空间包含 Thread 类型以及它的 Start() 和 Abort() 方法。下面的完整示例定义了 ThreadDemo.threadAssign(),创建带有 Main() 的 ThreadExampleMain 类,启动工作线程,并请求它停止。
循环配置为执行六次迭代,而不是四次。Thread is 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 会在终止请求之前输出;输出顺序以及 First Thread 行的数量取决于线程调度,因此不应期待固定的输出记录。依赖其行为之前,请在目标运行时上运行该示例。
在支持 Abort() 的运行时中,工作线程会收到异步的 ThreadAbortException 并停止。这与 Main() 中由 Abort() 同步抛出的异常不同。在发出请求之前输出多少行 First Thread,以及这些行的确切顺序,取决于线程调度;循环本身允许执行六次迭代。如果 Abort() 抛出 PlatformNotSupportedException,工作线程可能继续运行,而 catch 分支会报告这次同步失败;该消息只是诊断信息,不能证明运行时在所有上下文中都不支持此 API。无论哪种情况,都不要把 Thread is abort 行当作线程已终止的证据。无论经过哪条路径,thread.Join() 都会等待工作线程完成后再让 Main() 返回;当不支持终止请求时,这意味着等待工作线程正常完成。
使用 CancellationToken 协作式停止线程
对于新代码,应协作式地停止线程,而不是强制终止。线程的拥有者创建一个 CancellationTokenSource,并将其 Token 传递给工作线程。工作线程在各工作单元之间的安全点检查该令牌,当它看到取消请求时,就不再开始新的工作,释放自己拥有的资源,然后返回。拥有者用 Cancel() 请求停止,然后调用 Join() 等待工作线程真正结束。取消只是一个请求:从不检查令牌的工作线程,或卡在一次长时间阻塞调用中的工作线程,在观察到令牌之前不会停止,因此应让每个步骤保持简短,或使用直接接受令牌的 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()可用的运行时上,异步异常可能在工作线程持有锁或只更新了一半共享数据时到达,使状态对所有其他线程来说都不一致。协作式取消避免了这一点,因为由工作线程自己选择停止的安全点。