如何在异步多任务场景下正确使用ArrayPool管理相机图像缓存
解决方案:用引用计数包装池化缓冲区实现自动归还
核心思路
创建一个封装池化数组的包装类,通过引用计数跟踪缓冲区的使用者数量,当最后一个使用者完成操作后自动将缓冲区归还到ArrayPool,既避免大对象堆(LOH)压力,又支持异步多场景使用。
具体实现
1. 池化缓冲区包装类
这个类负责管理数组的租用、引用计数和自动归还,实现IDisposable接口确保资源被正确释放:
public sealed class PooledImageBuffer : IDisposable { private readonly byte[] _pooledBuffer; private int _referenceCount; private bool _isReturnedToPool; // 存储实际图像大小,避免客户端访问池化数组的冗余部分 public int ActualImageSize { get; } // 对外提供只读的Memory<T>,防止修改原始缓冲区 public ReadOnlyMemory<byte> ImageData => _pooledBuffer.AsMemory(0, ActualImageSize); private PooledImageBuffer(byte[] pooledBuffer, int imageSize) { _pooledBuffer = pooledBuffer; ActualImageSize = imageSize; _referenceCount = 1; } // 从ArrayPool租用缓冲区并创建包装实例 public static PooledImageBuffer Rent(int requiredSize) { var buffer = ArrayPool<byte>.Shared.Rent(requiredSize); return new PooledImageBuffer(buffer, requiredSize); } // 复制引用(增加计数),供多个异步操作共享同一缓冲区 public PooledImageBuffer Share() { if (_isReturnedToPool) throw new ObjectDisposedException(nameof(PooledImageBuffer)); Interlocked.Increment(ref _referenceCount); return this; } // 释放引用(减少计数),计数归零时归还缓冲区 public void Dispose() { if (_isReturnedToPool) return; if (Interlocked.Decrement(ref _referenceCount) == 0) { ArrayPool<byte>.Shared.Return(_pooledBuffer); _isReturnedToPool = true; } } // 兜底Finalizer:防止客户端忘记Dispose导致缓冲区泄漏 ~PooledImageBuffer() { if (!_isReturnedToPool) { // 可在此添加日志,排查未正确释放的场景 ArrayPool<byte>.Shared.Return(_pooledBuffer); _isReturnedToPool = true; } } }
2. 修改相机采集循环
在循环中租用包装类,通过Share()方法传递给事件订阅者,确保原始实例的引用计数正确:
while (IsLive) { var imageSize = camera.GetParamInt(PRM.IMAGE_PAYLOAD_SIZE); using var rootBuffer = PooledImageBuffer.Rent(imageSize); // 将图像数据写入池化缓冲区 camera.GetImage(rootBuffer._pooledBuffer); // 向所有订阅者传递共享引用 ImageReceivedEvent?.Invoke(this, rootBuffer.Share()); }
3. 客户端使用示例
无论是同步UI更新还是异步图像处理,都通过using语句确保引用被正确释放:
WPF实时显示(同步)
private void OnImageReceived(object sender, PooledImageBuffer buffer) { using (buffer) { // 转换为WPF BitmapSource(仅使用实际图像大小的内存) var bitmap = BitmapSource.Create( imageWidth, imageHeight, 96, 96, PixelFormats.Bgr24, null, buffer.ImageData.Span, imageStride); // 切换到UI线程更新显示 Dispatcher.Invoke(() => LiveImageControl.Source = bitmap); } }
异步图像处理(多任务)
private async Task ProcessImageFeaturesAsync(PooledImageBuffer buffer) { using (buffer) { await Task.Run(() => { // 图像特征识别 var featureDetector = new FeatureDetector(); var features = featureDetector.Detect(buffer.ImageData.Span); // 处理识别结果 UpdateFeatureResults(features); }); } } // 同时触发多个异步任务 private void OnImageReceived(object sender, PooledImageBuffer buffer) { // 每个任务共享同一缓冲区,引用计数自动增加 _ = ProcessImageFeaturesAsync(buffer.Share()); _ = CheckFocusAsync(buffer.Share()); _ = ScanQrCodeAsync(buffer.Share()); }
关键注意事项
- 线程安全:使用
Interlocked操作确保引用计数在多线程场景下的正确性 - 避免越界访问:通过
ImageData暴露只读的Memory<T>,仅包含实际图像数据的范围 - 异常安全:
using语句确保即使客户端代码抛出异常,也会正确释放引用 - 兜底机制:Finalizer作为最后防线,防止因客户端忘记调用
Dispose导致的缓冲区泄漏
内容的提问来源于stack exchange,提问作者Joe
相关产品推荐
相关产品推荐

