6.23:_cts.Cancel,_cts.Token;死锁

语法1

对!完全正确。

一句话

_cts.Cancel() 一调用,所有从它出来的 token 的 IsCancellationRequested 都变成 true。

关系图

code复制 复制代码
CancellationTokenSource _cts = new CancellationTokenSource();
        ↓ 创建
CancellationToken token = _cts.Token;
        ↓
多个地方拿到同一个 token
        ↓
用户点"停止"按钮 → _cts.Cancel()
        ↓
token.IsCancellationRequested → true(所有 token 同时变)

代码示例

csharp复制 复制代码
// 创建源
CancellationTokenSource _cts = new CancellationTokenSource();

// 拿到 token(可以分发给多个任务)
CancellationToken token1 = _cts.Token;
CancellationToken token2 = _cts.Token;

// 此时都是 false
Console.WriteLine(token1.IsCancellationRequested);  // false
Console.WriteLine(token2.IsCancellationRequested);  // false

// 用户点停止
_cts.Cancel();

// 瞬间全部变 true
Console.WriteLine(token1.IsCancellationRequested);  // true
Console.WriteLine(token2.IsCancellationRequested);  // true

为什么这样设计?

code复制 复制代码
一个 _cts(控制中心)
    ↓
发给多个任务(每个任务拿一个 token)
    ↓
用户点停止 → _cts.Cancel()
    ↓
所有任务同时收到"停工"通知

你的项目里

csharp复制 复制代码
// 用户点"停止扫描"按钮
private void StopButton_Click(object sender, RoutedEventArgs e)
{
    _cts.Cancel();  // 一个命令,所有扫描任务同时停下
}

一句话总结

_cts.Cancel() 是"喊停工",token.IsCancellationRequested 是"听到停工"。一个喊,所有 token 同时听到。

code 复制代码
_cts = 总闸开关
_cts.Cancel() = 拉总闸

拉总闸(Cancel)→ 所有灯(token)全灭
csharp 复制代码
// 1. 造一个停工令牌模板
CancellationTokenSource _cts = new();

// 2. 从模板里造停工令牌,发给任务
var scanTask = RunScanTask(_cts.Token);   // 给任务1
var otherTask = DoSomething(_cts.Token);  // 给任务2

// 3. 用户点停止 → 按停工按钮
_cts.Cancel();  // 模板一按,所有令牌同时生效

// 4. 所有任务检查令牌
if (token.IsCancellationRequested)  // "令牌响了吗?"
    return;  // 停了

问题2

两个问题都很关键,逐个回答:

问题1:MoveToAsync 不能自己造 token 吗?

csharp 复制代码
// MoveToAsync 内部自己造一个
public async Task MoveToAsync(double targetX, double targetY)
{
    var myCts = new CancellationTokenSource();  // 自己造的
    CancellationToken myToken = myCts.Token;

    await Task.Delay(1000, myToken);
}

能造,但没用!

code复制 复制代码
问题:谁来按 myCts.Cancel()?

用户点停止按钮 → _cts.Cancel()  → 主流程的 token 响了 ✅
                              → MoveToAsync 的 myToken 没响 ❌
                                (没人按 myCts.Cancel())

自己的停工按钮,用户按不到!
code复制 复制代码
对讲机类比:
MoveToAsync 自己买了个对讲机
但频道和工头的对讲机不一样
工头喊停工 → 听不到!

问题2:同一个 token = 一按全停 ✅

你理解得完全对!传同一个 token 就是为了让"一按全停"。

csharp复制 复制代码
// StartScanInternal
_cts = new CancellationTokenSource();
CancellationToken token = _cts.Token;  // 从 _cts 取出令牌

// 把同一个令牌传给 MoveToAsync
await _scanAxis.MoveToAsync(TargetX, TargetY, token);
//                                              ↑
//                                     同一个令牌!
code复制 复制代码
用户点停止 → _cts.Cancel()
    ↓
    ├─ StartScanInternal 的 token → 响了 ✅
    └─ MoveToAsync 的 token       → 也响了 ✅(因为是同一个)
        ↓
        Task.Delay(1000, token) → 立即抛异常退出

用你的话总结

code复制 复制代码
_cts 是一个停工模板
token 是从 _cts 出来的停工令牌
_cts.Cancel() 是按停工按钮

把 token 传给 MoveToAsync:
→ StartScanInternal 和 MoveToAsync 拿着同一个令牌
→ 用户一按停止(_cts.Cancel())
→ 两边同时听到停工信号
→ 一按全停 ✅

对比

一句话总结

问题1:自己造的 token 用户按不到,没意义。问题2:对!传同一个 token 就是让用户一按停止,StartScanInternal 和 MoveToAsync 同时停。一按全停!

语法3

死锁场景(ConfigureAwait(true) 默认)

两步演示

第一步:UI 线程调用 async 方法

csharp复制 复制代码
// UI 线程(按钮点击事件)
private async void ScanButton_Click(object sender, RoutedEventArgs e)
{
    await _scanAxis.MoveToAsync(TargetX, TargetY, token);  // ← UI线程等
    // 这行要等 MoveToAsync 完成才继续
}

第二步:MoveToAsync 内部(ConfigureAwait(true))

csharp复制 复制代码
public async Task MoveToAsync(...)
{
    _ecatMaster.WritePD0(ServoAxis, $"移动到X:{targetX}");

    await Task.Delay(1000);  // ← 内部也用默认 ConfigureAwait(true)

    Posx = targetX;  // 要回 UI 线程才能执行这行
}

第三步:死锁发生

code复制 复制代码
UI线程:点了扫描按钮
    ↓
UI线程:进入 MoveToAsync,然后 await Task.Delay(1000)
    ↓
UI线程被"卡住"(在等 MoveToAsync完成),但同时 MoveToAsync 内部也在等
回到UI线程才能继续(ConfigureAwait(true) 的要求)
    ↓
互相等 → 死锁 💀

UI线程:我想回 MoveToAsync继续
MoveToAsync:我要等UI线程才能继续
    ↓
两个人互相等 → 死锁

ConfigureAwait(false) 怎么破?

csharp复制 复制代码
public async Task MoveToAsync(...)
{
    _ecatMaster.WritePD0(ServoAxis, $"移动到X:{targetX}");

    // 加了 .ConfigureAwait(false)
    await Task.Delay(1000).ConfigureAwait(false);  // ← 不等UI线程

    Posx = targetX;  // 不用等 UI 线程,在线程池直接执行
}
code复制 复制代码
UI线程:点了扫描按钮
    ↓
UI线程:进入 MoveToAsync,await Task.Delay
    ↓
Task.Delay(1000) 开始倒计时,UI线程"解放"了(可以干别的)
    ↓
1秒后,Task.Delay完成
    ↓
ConfigureAwait(false) → 不用回UI线程,在线程池继续
    ↓
Posx = targetX 直接在线程池执行
    ↓
MoveToAsync完成,UI线程继续执行 ScanButton_Click 下一行
    ↓
没有死锁 ✅

对比图

code复制 复制代码
ConfigureAwait(true) 死锁:
UI线程 ──等──→ MoveToAsync完成
   ↑                    │
   │                    ↓
   └──── 等回UI线程 ────┘
       互相等 → 💀

ConfigureAwait(false) 不死锁:
UI线程 ──进去──→ MoveToAsync开始 → await时UI线程解放
                     ↓
               线程池继续执行后续代码
                     ↓
               MoveToAsync完成 → UI线程继续
       没有互相等 ✅

一句话总结

死锁 = 两个人互相等:UI 线程在等 MoveToAsync 完成,MoveToAsync 内部在等回 UI 线程。ConfigureAwait(false) 让 MoveToAsync 不用等回 UI 线程,在线程池干完就行,打破互相等。

语法4

复制代码
            while (!token.IsCancellationRequested && !_isDisposed &&sw.ElapsedMilliseconds < maxWaitMs)

这段代码在 MoveToAsync 返回后,每 200ms 检查一次轴是否真的到位,最多等 10 秒。到位了就继续,超时或取消就退出。

就是防止无限等待------轴卡住了、编码器坏了、通讯断了,循环会一直跑,永远等不到到位信号。

没有超时会怎样?

code复制 复制代码
场景:编码器坏了,一直返回 0,永远读不到目标值

while (!token.IsCancellationRequested && !_isDisposed)
{
    double curX = _scanAxis.Posx;  // 永远返回 0
    if (Math.Abs(curX - targetX) < 0.1)  // |0 - 100| > 0.1,永远不成立
    {
        arrived = true;
        break;  // 永远跳不出去!
    }
    await Task.Delay(200);
}

程序卡在这里,永远无法退出 💀

有超时会怎样?

csharp复制 复制代码
while (... && sw.ElapsedMilliseconds < maxWaitMs)  // ← 多了这个条件
code复制 复制代码
场景:编码器坏了,永远返回 0

t=0      开始等
t=200ms  第1次检查:不到位,继续
t=400ms  第2次检查:不到位,继续
...
t=9998ms 第50次检查:不到位,继续
t=10000ms while条件判断:sw.ElapsedMilliseconds >= 10000 → 条件不满足
t=10000ms 退出 while 循环 ✅

程序不会被卡死!

三个退出条件的作用

code复制 复制代码
用户按停止    → 正常取消 ✅
对象已销毁    → 正常退出 ✅
超时10秒      → 异常情况保底 ✅

没有超时 → 轴卡住了程序就卡死
有超时   → 10秒后自动退出,不卡死

类比

code复制 复制代码
maxWaitMs = 10000
= 约号后最多等 10 分钟,过了就换下一家

没有这个:
  等的人永远不来 → 一直等 → 等到天荒地老 → 卡死 💀

有这个:
  等了10分钟人还没来 → 走了干别的 → 不卡死 ✅

一句话总结

超时 = 保险:轴卡住、编码器坏、通讯断等异常情况时,10秒后自动退出,避免无限等待卡死程序。

语法6

logCounter 是日志节流------避免等到位时刷屏。

问题:到位检测要等多久?

code复制 复制代码
目标位置:100mm
当前位置:0mm
每 200ms 走 10mm

t=0     0mm    → 没到位
t=200ms 10mm   → 没到位
t=400ms 20mm   → 没到位
t=600ms 30mm   → 没到位
...
t=2000ms 100mm  → 到位了!

共检查了 11 次(0, 200, 400, ..., 2000ms)

如果每次都打印日志

csharp复制 复制代码
while (...)
{
    double curX = _scanAxis.Posx;
    WeakReferenceMessenger.Default.Send(new LogMessage($"等待到位... curX={curX:F1}"));
    // ↑ 每次检查都发日志!
}
code复制 复制代码
20:00:00.001 等待到位... curX=0.0
20:00:00.201 等待到位... curX=10.0
20:00:00.401 等待到位... curX=20.0
20:00:00.601 等待到位... curX=30.0
20:00:00.801 等待到位... curX=40.0
20:00:01.001 等待到位... curX=50.0
...

刷屏了!日志面板被淹没 💀

logCounter 怎么解决?

csharp复制 复制代码
int logCounter = 0;  // 计数器从 0 开始

while (...)
{
    double curX = _scanAxis.Posx;
    if (Math.Abs(curX - targetX) < 0.1) { break; }

    await Task.Delay(200);

    logCounter++;  // 每次+1
    if (logCounter % 5 == 0)  // 每5次才打印一次
    {
        WeakReferenceMessenger.Default.Send(new LogMessage($"等待到位... curX={curX:F1}"));
    }
}

效果对比

code复制 复制代码
logCounter = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
logCounter % 5 =  0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0
打印?        =  ✅, -, -, -, -, ✅, -, -, -, -, ✅

只有 logCounter 是 5 的倍数时才打印

= 每5次打印1次

= 减少了 80% 的日志量

为什么用 % 5(取余)?

csharp复制 复制代码
logCounter % 5 == 0
code复制 复制代码
取余 = 看看能被5整除吗
logCounter = 5  → 5 % 5 = 0 → 能整除 → 打印 ✅
logCounter = 10 → 10 % 5 = 0 → 能整除 → 打印 ✅
logCounter = 15 → 15 % 5 = 0 → 能整除 → 打印 ✅

logCounter = 6  → 6 % 5 = 1 → 不能整除 → 不打印 ❌
logCounter = 7  → 7 % 5 = 2 → 不能整除 → 不打印 ❌

类比

code复制 复制代码
logCounter = 0 就是"计数器从0开始"
logCounter++ = "计数器+1"
logCounter % 5 == 0 = "每到第5个就干一次"

比如食堂打饭:

阿姨喊"每5个人才打一次菜"

你数:1, 2, 3, 4, 5(打菜), 6, 7, 8, 9, 10(打菜)...

一句话总结

logCounter 是日志节流计数器:每 5 次检查才发 1 条日志,避免等到位过程中刷屏日志。

语法6

用户点停止时,跳出 while 循环。

执行流程

csharp复制 复制代码
while (...)
{
    curX = _scanAxis.Posx;
    if (到位) break;
    
    try
    {
        await Task.Delay(200, token).ConfigureAwait(false);
    }
    catch
    {
        break;  // 用户取消了,跳出循环
    }
}

用户点停止

_cts.Cancel() → token 被取消 → Delay 抛 OperationCanceledException → catch 捕获 → break 跳出循环

为什么用 catch { break; }

用户点停止后:

不需要继续轮询了

直接跳出 while 循环

执行后续的清理逻辑(恢复状态为 Idle 等)

break 跳到哪里

csharp复制 复制代码
while (...)
{
    ...
    try
    {
        await Task.Delay(200, token);
    }
    catch
    {
        break;  // ← 跳出 while
    }
}

// ← break 跳到这里
// 后续代码(日志、状态清理等)
if (arrived)
{
    // 到位了,拍照
}
else
{
    // 被取消或超时
}

本质:用户取消 → 异常 → catch 捕获 → break 退出轮询。

对!全局一个 _cts,一按全停。

核心原理

csharp复制 复制代码
// 一个 _cts
private CancellationTokenSource _cts;

// 但可以生成多个 token

_cts.Token → 传给方法A

_cts.Token → 传给方法B

_cts.Token → 传给方法C

一键全停

code复制 复制代码
用户点停止 → _cts.Cancel()
    ↓
所有用 _cts.Token 的地方同时收到取消信号:
    ↓
方法A:token.IsCancellationRequested = true
方法B:Task.Delay(..., token) 抛异常
方法C:while 循环检测到 token 被取消
    ↓
全部停止

项目中的例子

csharp复制 复制代码
// StartScanInternal
await RunScanTask(targetX, targetY, _cts.Token);

// RunScanTask
await _scanAxis.MoveToAsync(..., token);

// MoveToAsync
await Task.Delay(1000, token);

// while 轮询
await Task.Delay(200, token);

同一个 _cts.Token 传遍了整个调用链。

用户点停止

code复制 复制代码
_cts.Cancel()
    ↓
MoveToAsync 内部的 Task.Delay(1000, token) 被取消
while 循环的 Task.Delay(200, token) 被取消
所有带 token 的地方同时停止

一个 _cts,一个 Cancel(),所有关联任务全部停止。