
多相机并行采图最佳实践:Task.WhenAll + 异常处理 + 资源释放
- [多相机并行采图最佳实践: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遇到任何一个任务抛出异常,会在所有任务结束后把异常抛出来。但问题是------其他相机明明拍好了,结果就因为一个相机掉线,整批图都不要了?产线可不会同意。
两种处理方式:
- 每个任务自己做异常捕获,返回一个包含"成功标志+图像数据"的结构体,不让异常往上抛。
- 用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连个像样的异步接口都不给,全是同步阻塞+回调,包装起来是真费劲。但为了产线效率,这功夫还是得花。