C# 里取消 Task 的正规做法是协作式取消:调用方拿 `CancellationTokenSource` 发出取消,任务内部用 `CancellationToken` 在安全的检查点自己停下来。不要用 `Thread.Abort`,也不要靠 `Dispose` 任务来取消。
怎么写
调用方创建取消源,把 Token 传进任务,需要停的时候调用 `Cancel()`,然后仍然 `await` 这个任务,让它做完收尾。
```csharp
using var cts = new CancellationTokenSource();
try
{
await RunAsync(cts.Token);
}
catch (OperationCanceledException)
{
// 这是取消,不是故障
}
void Stop() => cts.Cancel();
async Task RunAsync(CancellationToken ct)
{
while (true)
{
ct.ThrowIfCancellationRequested();
await DoOneStepAsync(ct); // 支持 Token 的 API 把 Token 继续传下去
await Task.Delay(10, ct);
}
}
```
`ThrowIfCancellationRequested()` 会抛出 `OperationCanceledException`。捕获它表示「被人停了」;别的异常才是失败。取消之后不要把流程记成成功。
为什么说它优雅
`Cancel()` 只是把标记设为已取消,不会把线程掐死。任务跑到下一次检查 Token 的地方才停,所以不会停在写到一半的串口帧或运动指令中间。
支持 Token 的 API 会自己停:`Task.Delay`、`ReadAsync`、`WaitAsync`、`Task.Run` 的重载。Token 要一路传到最底层,传断了,底层仍会把当前这次 IO 做完。
超时和停止按钮可以合成一个 Token:
```csharp
using var stopCts = new CancellationTokenSource();
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
stopCts.Token, timeoutCts.Token);
await MoveAndWaitAsync(linked.Token);
```
任一来源取消,任务都会看到。`CancelAfter` 则是同一个源上的超时。
收尾不要写在 `catch` 里碰运气。用 `finally`,或者 `token.Register(() => StopAxis())`,保证取消时能发减速停。`Register` 的回调可能跑在任意线程上,里面不要动 UI 控件。
上位机里怎么用
自动流程的等待循环带着 Token。停止按钮调用 `Cancel()`。通信线程在下一次检查时看到取消,再调用 `dmc_stop` 或写 PLC 停机位,然后抛出取消异常,状态机进入「已停止」。不要在 UI 线程里 `while` 等运动完成,也不要 `Abort` 那条正在调运动卡的线程。