开发手记 · 第 1 / 9 篇 合集目录 下一篇 ›

翻一台常年开在工业现场的测控设备配套程序时,我先是被一段循环卡住了。

那段采集线程的主循环长这样(等价改写,名字换掉了):

while (!_stopRequested)
{
    if (_acquiring)
    {
        var samples = _device.ReadAllValues();
        _buffer.Add(samples);
    }
    // 没有 else,也没有 Sleep
}

条件不满足的时候,它在做一件事:以能达到的最快速度,反复问要不要采。没活的时候也不歇。而且它的优先级是 Highest,UI 线程想刷个界面都得排在它后面。

我顺手跑了三条命令确认范围:

grep -rn "Priority = ThreadPriority" --include=*.cs .
grep -rn "\.Suspend()" --include=*.cs .
grep -rn "Thread.Abort" --include=*.cs .

命中结果是优先级拉满 5 处,Suspend 4 处,Abort 4 处。C# / WPF、net48,全仓 306 个文件、3.2 万行。

我没法确认作者当时怎么想,但更可能的原因是这个:Sleep(1) 在 Windows 上可能实际睡到 10 多毫秒,他担心采集延迟,于是干脆不睡。这个推演在"数据一直来"的时候成立,可设备大部分时间并不在采集,上料、下料、等待都占时间。一个为数据洪峰设计的循环,被放在九成时间没数据的场景里裸奔。

站在机器旁边听到的风扇声,可能就是它发出的。它没在采数据,只是在问。

同一个文件里还有两个更让我在意的东西。暂停用 Thread.Suspend(),停止用 Thread.Abort()。这两个 API 在 .NET Core 上直接不支持,会抛异常,也就是说代码被钉死在老框架上。Suspend 会在任意指令点把线程挂起,如果那一刻正握着一把锁,锁就再也不释放;Abort 会让 finally 里的清理不一定跑得完。

改起来不复杂:停止改用协作式的取消令牌,暂停改用事件通知,循环里加一句很短的休眠兜底。改完之后 CPU 从两个核降到接近零,界面也不卡了,采集实时性一点没变差,因为真有数据来的时候那个循环本来就不走空转分支。

我后来在别的项目里又见过几次类似的写法,无一例外都是在"要快"这个念头下写出来的。大家思路出奇地一致:既然慢是问题,那就让它一直跑着,别让它停下来等。

真正混掉的是两件事,随时能干和正在干。一个待命的人,和一个原地冲刺的人,消耗完全不一样。