语法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(),所有关联任务全部停止。
