Loading...
时易うさぎのBlog
闲聊工作经验

Task.Run踩坑记录-并不是万金油?

前言

在 C# 开发中,Task.Run() 几乎是很多人接触异步编程时最先学会的方法。

当遇到耗时操作时,很多人的第一反应就是:

Task.Run(() =>
{
    DoSomething();
});

把任务丢到后台执行,UI 不会卡顿,程序也能继续响应。

因此很容易形成一种习惯:

只要是耗时操作,都应该使用 Task.Run。

但是,在一些特殊场景下,这种思路可能会带来非常隐蔽甚至严重的问题。

尤其是在:

  • 运动控制

  • 机器人控制

  • 高速采集

  • 硬件同步

  • 实时通信

这些场景中,真正重要的往往不是"异步",而是:

确定的执行顺序,以及稳定的调度时间。

本文记录一次实际项目中,因为错误使用 Task.Run 导致运动控制异常的问题。


项目背景

当时参与的是一个 VR 显示模组 AA(Active Alignment)项目。

在显示模组装调过程中,需要通过多轴运动机构调整位置,使左右两个显示区域达到最佳匹配效果。

由于是双目结构,因此设备中存在两套独立运动轴:

  • 左眼调整轴

  • 右眼调整轴

两套轴需要同步运动。

理想状态下:

两个轴组应该严格按照相同的步骤执行:

任何一边出现明显不同步,都可能导致机构运动异常。


原项目中的实现方式

这个项目是中途接手的。

在之前的实现中,轴运动控制大量使用了 Task.Run()

代码逻辑类似:

Task.Run(() =>
{
    MoveAxis(position);
});

如果需要同时控制左右两套轴:

Task.Run(() =>
{
    LeftAxis.Move();
});

Task.Run(() =>
{
    RightAxis.Move();
});

从表面看,这种写法似乎没有问题:

  • 两个任务同时提交

  • 两边应该同时执行

但是实际运行过程中,经常出现:

  • 左轴先开始运动

  • 右轴延迟一段时间才开始

  • 两边运动时间差逐渐扩大

严重情况下甚至出现:

中间步骤 B 没有正常执行。

最终导致:

  • 运动轨迹异常

  • 定位错误

  • 机构碰撞风险增加


为什么 Task.Run 会出现这个问题?

很多人会误以为:

两个 Task.Run 几乎同时调用,所以两个任务应该同时执行。

但是实际上,Task.Run() 默认使用的是 .NET 线程池(ThreadPool)。

任务提交之后,并不是立即创建线程执行。

实际流程类似:

线程池保证的是:

任务最终会被执行。

但是它不保证:

  • 什么时候开始执行

  • 两个任务之间的时间差

  • 执行顺序是否完全符合预期

例如普通程序中:

任务1 延迟 30ms

任务2 延迟 50ms

用户基本感觉不到区别。

但是运动控制中:

左轴 0ms 开始

右轴 30ms 后开始

可能已经意味着:

  • 两个轴位置产生偏差

  • 同步状态失效

  • 后续步骤出现错误


更严重的问题:提交顺序不代表执行顺序

运动控制程序通常有明确流程:

开发者自然认为:

A 一定先执行,然后 B,最后 C。

但是如果内部逻辑依赖线程池调度:

实际情况可能变成:

因为线程池关注的是:

哪个线程当前空闲。

而不是:

哪个步骤必须优先完成。

对于普通业务逻辑,这可能只是性能波动。

但是对于设备控制:

这可能就是一次错误动作。


修改方案:使用独立运动线程

发现问题后,没有继续尝试优化 Task.Run。

原因很简单:

问题本质不是代码写法,而是选择了错误的执行模型。

运动控制本身就不适合交给线程池。

之后改为创建独立运动线程:

Thread motionThread =
    new Thread(MotionLoop);

motionThread.Start();

线程内部维护统一运动流程:

void MotionLoop()
{
    while (true)
    {
        var command = GetNextCommand();

        ExecuteMotion(command);
    }
}

整体结构变成:

这样可以保证:

  • 指令顺序固定

  • 运动流程可控

  • 不受线程池调度影响

  • 调试更加直观

修改完成后:

  • 左右轴不同步问题消失

  • 步骤跳过问题消失

  • 设备运行稳定


Task.Run 到底能不能用?

当然可以。

问题不是 Task.Run 不好。

真正的问题是:

不要把它用于需要确定性的任务。

适合 Task.Run 的场景

// Excel 导出
Task.Run(ExportExcel);
// 图片处理
Task.Run(ProcessImage);
// 大文件处理
Task.Run(ReadFile);
…………

这些让用户等待几秒没有问题。

重点是避免阻塞 UI。


不适合 Task.Run 的场景

运动控制

例如:

  • 电机控制

  • 多轴同步

  • 插补运动

要求:

  • 顺序准确

  • 时间稳定


硬件触发

例如:

  • 相机曝光

  • PZT 扫描

  • PLC 同步信号


高速实时采集

例如:

  • 高频传感器

  • 数据采集卡

这些任务需要:

  • 专用线程

  • 硬件同步

  • 实时控制策略

而不是线程池。


总结

Task.Run() 是一个非常优秀的工具。

它解决的问题是:

不阻塞当前线程。

但是它并不是:

让代码立即执行,并且保证执行时序。

在工业控制领域,需要关注的不只是:

  • 能不能运行

更重要的是:

  • 什么时候运行

  • 按什么顺序运行

  • 每一次运行是否一致

一次简单的 Task.Run(),在普通软件中可能完全没有问题。

但是在运动控制系统中,它可能决定设备是稳定运行,还是下一秒发生异常动作。


后记

这次问题也让我重新认识到:

软件开发中,没有绝对正确的技术。

Task.Run 不是错误。

错误的是:

把一个不保证实时性的工具,用在了必须确定性的场景。

异步不是万金油。

选择正确的执行模型,比选择更"高级"的技术更加重要。

分享到

评论