C#线程中接收HoloLens帧的最优方案及性能瓶颈解决
HoloLens帧传输帧率骤降问题解决
问题现象
通过单Socket连接向服务器传输HoloLens捕获的视频帧时:
- 初始使用
Task.Run发起发送任务,前期帧率正常,后续逐渐降至0,调试发现线程数量过载(大量线程堆积导致系统资源耗尽)。 - 改用
Parallel.Invoke后帧率仍偏低,发送任务的处理会干扰新帧的接收。 - 尝试async/await结合取消令牌方案,问题未得到缓解。
相关代码
C# 帧到达回调函数
private void ColorFrameReader_FrameArrivedAsync(MediaFrameReader sender, MediaFrameArrivedEventArgs args) { var frame = sender.TryAcquireLatestFrame(); if (frame != null) { SoftwareBitmap originalBitmap = null; var inputBitmap = frame.VideoMediaFrame?.SoftwareBitmap; if (inputBitmap != null) { // 转换为XAML控件支持的BGRA8预乘alpha格式 originalBitmap = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); SoftwareBitmap outputBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, originalBitmap.PixelWidth, originalBitmap.PixelHeight, BitmapAlphaMode.Premultiplied); // 调用发送逻辑 ab_func(originalBitmap); } Interlocked.Increment(ref _frameCount); } } async void ab_func(SoftwareBitmap fr) { await Task.Run(() => { _helper.Connect(fr); }); }
C++ Socket发送方法
void OpenCVHelper::Connect(SoftwareBitmap^ input) { Mat inputMat, outputMat; if (!(TryConvert(input))) { return; } int imgSize = GlobalMat.total() * GlobalMat.elemSize(); auto t0 = std::chrono::system_clock::now(); auto nanosec = t0.time_since_epoch(); if (ConnectSocket == INVALID_SOCKET) { WSACleanup(); return; } uchar* iptr = GlobalMat.data; t0 = std::chrono::system_clock::now(); nanosec = t0.time_since_epoch(); m_OutStream << "timestamp before sending of frame " << i++ << " = " << std::chrono::duration_cast<std::chrono::milliseconds>(nanosec).count() << std::endl; // 发送帧到服务器 if ((bytes = send(ConnectSocket, (char*)iptr, imgSize, 0)) < 0) { m_OutStream << "problem 2 ! \n"; } return; }
问题根源分析
- 线程池过载:每次帧到达就通过
Task.Run/Parallel.Invoke创建新任务,导致线程池线程数量激增,线程上下文切换开销剧增,最终拖垮帧率。 - 无并发控制:没有限制发送任务的并发数量,当Socket发送速度跟不上帧生成速度时,任务会持续积压,占用大量系统资源。
- 同步Socket阻塞:
send是同步阻塞调用,每个任务都会占用线程直到发送完成,进一步加剧线程资源消耗。
解决方案
1. 引入有界队列+固定线程处理
用阻塞队列缓存待发送的帧,仅用固定数量的线程处理发送逻辑,避免线程爆炸:
// 初始化有界阻塞队列,限制最大缓存帧数量(防止内存溢出) private readonly BlockingCollection<SoftwareBitmap> _frameQueue = new BlockingCollection<SoftwareBitmap>(boundedCapacity: 10); // 应用启动时启动固定发送线程 public void StartSenderThreads() { // 启动1个发送线程,可根据网络情况调整为2 Task.Run(() => { foreach (var frame in _frameQueue.GetConsumingEnumerable()) { _helper.Connect(frame); frame.Dispose(); // 及时释放帧资源 } }); } // 修改帧到达回调,仅将帧加入队列 private void ColorFrameReader_FrameArrivedAsync(MediaFrameReader sender, MediaFrameArrivedEventArgs args) { var frame = sender.TryAcquireLatestFrame(); if (frame != null) { SoftwareBitmap originalBitmap = null; var inputBitmap = frame.VideoMediaFrame?.SoftwareBitmap; if (inputBitmap != null) { originalBitmap = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); // 队列满时丢弃旧帧,避免阻塞帧捕获线程 if (!_frameQueue.TryAdd(originalBitmap)) { originalBitmap.Dispose(); } } Interlocked.Increment(ref _frameCount); frame.Dispose(); // 释放原生帧资源 } }
2. 替换同步Socket为异步发送
修改C++发送逻辑,使用异步WSASend避免线程阻塞:
void OpenCVHelper::AsyncConnect(SoftwareBitmap^ input) { Mat inputMat, outputMat; if (!(TryConvert(input))) { input->Dispose(); return; } int imgSize = GlobalMat.total() * GlobalMat.elemSize(); uchar* iptr = GlobalMat.data; if (ConnectSocket == INVALID_SOCKET) { WSACleanup(); input->Dispose(); return; } // 异步发送配置 WSABUF buf; buf.buf = (char*)iptr; buf.len = imgSize; DWORD bytesSent; DWORD flags = 0; OVERLAPPED overlapped = {0}; overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (WSASend(ConnectSocket, &buf, 1, &bytesSent, flags, &overlapped, NULL) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { m_OutStream << "Async send failed! \n"; CloseHandle(overlapped.hEvent); input->Dispose(); return; } } // 等待异步发送完成(实际场景可改用回调避免阻塞) GetOverlappedResult(ConnectSocket, &overlapped, &bytesSent, TRUE); CloseHandle(overlapped.hEvent); input->Dispose(); }
3. 强化帧资源管理
- 在帧到达回调中及时调用
frame.Dispose()释放原生资源,避免内存泄漏。 - 队列中的帧处理完成后立即释放,减少内存占用。
内容的提问来源于stack exchange,提问作者Nifty
相关产品推荐
相关产品推荐

