多相机并行采图最佳实践:Task.WhenAll + 异常处理 + 资源释放

多相机并行采图最佳实践:Task.WhenAll + 异常处理 + 资源释放

多相机并行采图最佳实践:Task.WhenAll + 异常处理 + 资源释放

头一回做四相机同步的项目,我写出来的代码大概是这样的:

csharp 复制代码
var img1 = cam1.GetOneFrame();
var img2 = cam2.GetOneFrame();
var img3 = cam3.GetOneFrame();
var img4 = cam4.GetOneFrame();

逻辑没毛病吧?四台相机挨个拍一遍,图拿齐了再去分析。

结果一跑起来傻眼了:第一台相机50ms出图,第二台等第一台完事再开始曝光,再等50ms,第三台再等......四台相机跑一圈下来200ms。产线节拍才要求40ms处理一整个工件,连一张图的传输时间都不够。

不是相机慢,是代码把它写成了流水线------后面的人必须等前面完事才能动手。

一、错在哪?你把并发搞成串行了

每台相机的"曝光+传输"是独立的一件事。A相机曝光的时候,B相机完全可以同时曝光,互不干扰。但上面的代码写法,cam2.GetOneFrame()必须等到cam1那一行完事才执行------不是因为技术限制,是代码逻辑本身就是顺序的。

改起来也不复杂:别一行一行等着,把每个采集动作包装成独立任务,然后一次性等所有任务完成。

csharp 复制代码
var t1 = Task.Run(() => cam1.GetOneFrame());
var t2 = Task.Run(() => cam2.GetOneFrame());
var t3 = Task.Run(() => cam3.GetOneFrame());
var t4 = Task.Run(() => cam4.GetOneFrame());

var images = await Task.WhenAll(t1, t2, t3, t4);

总耗时从"四台加起来"变成了"最慢那台的时间"。四台相机同时曝光、同时传输,理想情况下总耗时≈单台耗时。

那个项目换了这种方式之后,四台相机总耗时从200ms压到了55ms。多出来的5ms是任务调度开销,可以忽略。

二、踩过的坑,一个一个说

坑一:WhenAll不是自动帮你启动任务

上面写法里,Task.Run那一步已经启动任务了,WhenAll只负责"等待"。有人会写成这样:

csharp 复制代码
var t1 = cam1.GetOneFrameAsync();  // 假设这返回Task
var t2 = cam2.GetOneFrameAsync();
await Task.WhenAll(t1, t2);

这里关键是GetOneFrameAsync这个函数内部必须已经开始执行了。如果你在里面写的是new Task(...)但没调用Start(),那WhenAll等到天荒地老也不会完成。

我一般直接在函数开头就把异步操作跑起来,比如海康那个包装:

csharp 复制代码
Task<byte[]> GrabAsync(MyCamera cam)
{
    var tcs = new TaskCompletionSource<byte[]>();
    cam.MV_CC_RegisterImageCallBackEx_NET((pData, info, pUser) => {
        // 拷贝数据,tcs.SetResult
    }, IntPtr.Zero);
    cam.MV_CC_StartGrabbing_NET();
    return tcs.Task;  // 回调还没触发,但任务已经在"等待"状态了
}

StartGrabbing一调用,相机就开始干活了。TaskCompletionSource的任务是"未完成"状态,等回调里SetResult才变完成。WhenAll等的就是这个。

坑二:一个相机掉线,其他相机的图也跟着丢

默认情况下,WhenAll遇到任何一个任务抛出异常,会在所有任务结束后把异常抛出来。但问题是------其他相机明明拍好了,结果就因为一个相机掉线,整批图都不要了?产线可不会同意。

两种处理方式:

  1. 每个任务自己做异常捕获,返回一个包含"成功标志+图像数据"的结构体,不让异常往上抛。
  2. 用WhenAll拿到结果后,把失败的过滤掉,成功的继续用。

我比较喜欢第一种:

csharp 复制代码
public class CaptureResult
{
    public bool Success { get; set; }
    public byte[] ImageData { get; set; }
    public string CameraId { get; set; }
    public string ErrorMessage { get; set; }
}

async Task<CaptureResult> SafeGrabAsync(ICamera cam)
{
    try 
    {
        var img = await cam.GetOneFrameAsync();
        return new CaptureResult { Success = true, ImageData = img };
    }
    catch (Exception ex)
    {
        return new CaptureResult { Success = false, ErrorMessage = ex.Message };
    }
}

然后WhenAll拿到四个结果,遍历一下,成功的图拿去分析,失败的记日志并报警。至少产线不用停。

坑三:资源释放和取消

如果你程序中途想停止采集(比如用户点了停止按钮),正在执行的任务需要能被取消。Task.WhenAll本身不提供"取消等待"的功能,需要配合CancellationToken。

每个采集任务内部要定期检查ct.IsCancellationRequested,或者在等待底层API时传入token。海康、Basler的异步取图方法一般都支持传CancellationToken,记得用上。

另外,相机打开后占用的资源一定要在任务结束时释放。可以用finally或者自己实现Dispose模式,别指望着GC帮你收拾。

三、什么时候用WhenAll,什么时候用WhenEach

.NET 9出了个Task.WhenEach,可以依次处理完成的任务,不用等所有任务都完成。

区别举个例子:

  • WhenAll:点了四份外卖,必须等四份都到了才开吃。
  • WhenEach:哪份先到就先吃哪份,不用等后面的。

如果你的项目是"四台相机拍同一个工件,缺一张图都不能开始分析",用WhenAll。如果每个相机的图像独立处理、互不影响(比如四路监控),用WhenEach体验更好------处理延迟更低。

不过目前大部分工业项目还在.NET Framework 4.8或者.NET 6上混着,WhenEach只有.NET 9能用,先当个知识点存着吧。

四、不同SDK的异步包装

现实是:不是所有相机的SDK都直接提供async方法。

Basler pylon :最省心,有现成的RetrieveResultAsync,直接await。

海康MVS:只有同步回调。得自己用TaskCompletionSource包一层,代码量不大,但要注意回调里别卡太久,只做数据拷贝和SetResult。

堡盟GAPI :类似Basler,有GetBufferAsync,直接就能用。

通用建议 :把异步包装层和业务逻辑层分开。别在界面代码里到处写new TaskCompletionSource,封装成一个CameraService类,对外统一暴露Task<Image>

五、别忘了并发上限

WhenAll不会限制同时执行的任务数量。你一口气丢100个任务进去,线程池会创建一堆线程,上下文切换开销能把CPU干到100%。

实测经验:同时并发的任务数不建议超过环境逻辑核心数的两倍。超过的话自己分批处理:

csharp 复制代码
var batchSize = 4;
for (int i = 0; i < cameras.Count; i += batchSize)
{
    var batch = cameras.Skip(i).Take(batchSize);
    var tasks = batch.Select(c => c.GrabAsync());
    var results = await Task.WhenAll(tasks);
    // 处理这一批的结果
}

8台相机分两批,每批WhenAll,总耗时只是多了批次切换的开销,不会像顺序执行那样累加。

六、最后说句实在的

多相机并行的核心不是用什么API,是脑子里别塞着"顺序"这根弦。当你意识到四台相机可以同时曝光、同时传输,Task.WhenAll就是个很自然的工具。

别怕包装异步,别忽略异常处理,记得限制并发数。把这些细节处理好了,四台相机并行的总耗时从200ms压到50ms不是梦。

顺带吐槽一句:有些国产相机的SDK连个像样的异步接口都不给,全是同步阻塞+回调,包装起来是真费劲。但为了产线效率,这功夫还是得花。

相关推荐
Tom·Ge9 小时前
【AI前沿】2026.08.17 SpaceX 600亿收购Cursor正式完成·DeepSeek V4 Pro万亿开源·人形机器人生态全面爆发
人工智能·大模型·ai前沿
郑州光合科技余经理10 小时前
本地生活服务系统:成品模块和定制接口怎么划界
java·前端·人工智能·后端·系统架构·php·ai编程
2603_9651481110 小时前
家居百货蓝海:API挖掘高复购率生活小商品
大数据·服务器·人工智能·python·生活
萧瑟余晖10 小时前
Java深入解析篇三十三之密封类与接口(Sealed Classes)详解
java·开发语言
2501_9304724410 小时前
踩坑|CodeBuddy权限配置:AI误删文件、乱跑命令、.env泄露怎么防
人工智能·ai编程
小淮AI13 小时前
从“刷题”到“追问”:AI课堂正在重塑哪些学习旧习惯?
人工智能·学习
anlog13 小时前
ini文件读取,EXE同目录文件读取
c#·ini
cui_ruicheng14 小时前
LangChain 应用开发(十四):Agent 上下文与记忆机制
服务器·人工智能·python·langchain
论迹复利14 小时前
FreeRTOS 在 RISC-V 上是如何“点火“的 —— 从 main() 到第一个任务的完整链路
java·开发语言·risc-v
rockey62714 小时前
AScript之编译递归函数
c#·.net·script·动态脚本