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() 可用的运行时上,异步异常可能在工作线程持有锁或只更新了一半共享数据时到达,使状态对所有其他线程来说都不一致。协作式取消避免了这一点,因为由工作线程自己选择停止的安全点。