C#实现高速二进制数据文件写入,解决外部设备缓冲区堵塞问题
解决高速率二进制数据写入避免设备缓冲区堵塞的方案
核心问题分析
你当前的代码在数据接收事件回调里同步执行文件写入,磁盘IO的速度(尤其是机械盘)很容易跟不上30-40MB/s的接收速率,导致接收线程被阻塞,设备的小缓冲区来不及清空就会溢出丢数据。
最优解决方案:接收与写入解耦
把数据接收(生产者)和文件写入(消费者)分成两个独立的任务/线程,让接收线程只负责快速缓存数据,写入线程专门处理磁盘IO,彻底避免IO阻塞接收流程。下面提供两种易实现的方案:
方案1:双缓冲区(乒乓缓冲)
适合固定块大小的场景,逻辑简单,内存开销可控:
- 准备两个大小相同的缓冲区(比如128MB,可根据内存情况调整)
- 接收线程往当前活跃的缓冲区写数据,写满后切换到另一个缓冲区,同时通知写入线程处理刚写满的缓冲区
- 写入线程写完当前缓冲区后,等待下一个缓冲区的通知
代码示例:
// 定义双缓冲区和线程同步对象 private byte[] _buffer1; private byte[] _buffer2; private byte[] _currentReceiveBuffer; private int _currentBufferPosition; private readonly object _bufferLock = new object(); private readonly AutoResetEvent _bufferReadyEvent = new AutoResetEvent(false); private FileStream _fileStream; private bool _isRunning = true; // 初始化 public void Initialize(string fileName) { // 缓冲区大小设为128MB,可根据内存调整 int bufferSize = 128 * 1024 * 1024; _buffer1 = new byte[bufferSize]; _buffer2 = new byte[bufferSize]; _currentReceiveBuffer = _buffer1; _currentBufferPosition = 0; // 优化FileStream参数:顺序扫描、大缓冲区、异步友好 _fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.SequentialScan); // 启动写入线程 Task.Run(WriteBufferLoop); } // 数据接收回调(生产者) void Handler_OnFtdiBytesReceived(object sender, FtdiBytesReceivedEventArgs e) { if (!_isRunning) return; lock (_bufferLock) { // 计算当前缓冲区剩余空间 int remainingSpace = _currentReceiveBuffer.Length - _currentBufferPosition; if (e.NumBytesAvailable <= remainingSpace) { // 数据能放下,直接拷贝 Array.Copy(e.Bytes, 0, _currentReceiveBuffer, _currentBufferPosition, (int)e.NumBytesAvailable); _currentBufferPosition += (int)e.NumBytesAvailable; } else { // 先填满当前缓冲区 Array.Copy(e.Bytes, 0, _currentReceiveBuffer, _currentBufferPosition, remainingSpace); int remainingBytes = (int)e.NumBytesAvailable - remainingSpace; // 切换缓冲区 _currentReceiveBuffer = _currentReceiveBuffer == _buffer1 ? _buffer2 : _buffer1; // 通知写入线程处理满的缓冲区 _bufferReadyEvent.Set(); // 拷贝剩余数据到新缓冲区 Array.Copy(e.Bytes, remainingSpace, _currentReceiveBuffer, 0, remainingBytes); _currentBufferPosition = remainingBytes; } // 如果缓冲区已满,直接通知写入 if (_currentBufferPosition == _currentReceiveBuffer.Length) { _currentReceiveBuffer = _currentReceiveBuffer == _buffer1 ? _buffer2 : _buffer1; _bufferReadyEvent.Set(); _currentBufferPosition = 0; } } } // 写入循环(消费者) private void WriteBufferLoop() { while (_isRunning) { // 等待缓冲区就绪通知 _bufferReadyEvent.WaitOne(); lock (_bufferLock) { // 确定要写入的缓冲区 byte[] bufferToWrite = _currentReceiveBuffer == _buffer1 ? _buffer2 : _buffer1; // 写入文件(用异步写入可进一步降低线程阻塞) _fileStream.Write(bufferToWrite, 0, bufferToWrite.Length); } } // 处理最后剩余的数据 lock (_bufferLock) { if (_currentBufferPosition > 0) { byte[] finalBuffer = _currentReceiveBuffer == _buffer1 ? _buffer1 : _buffer2; _fileStream.Write(finalBuffer, 0, _currentBufferPosition); } _fileStream.Flush(); _fileStream.Close(); } } // 停止操作 public void Stop() { _isRunning = false; // 触发最后一次写入通知 _bufferReadyEvent.Set(); }
方案2:基于ConcurrentQueue的生产者-消费者队列
适合数据块大小不固定的场景,灵活性更高:
- 用
ConcurrentQueue<byte[]>存储接收到的数据块(推荐用内存池复用数组,避免频繁GC) - 接收线程把数据块放入队列
- 写入线程不断从队列取出数据块写入文件
代码示例:
private readonly ConcurrentQueue<byte[]> _dataQueue = new ConcurrentQueue<byte[]>(); private FileStream _fileStream; private bool _isRunning = true; // 初始化 public void Initialize(string fileName) { _fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.SequentialScan); Task.Run(WriteQueueLoop); } // 数据接收回调 void Handler_OnFtdiBytesReceived(object sender, FtdiBytesReceivedEventArgs e) { if (!_isRunning) return; // 拷贝数据到新数组(用ArrayPool<byte>复用数组可优化内存) byte[] dataBlock = new byte[e.NumBytesAvailable]; Array.Copy(e.Bytes, 0, dataBlock, 0, (int)e.NumBytesAvailable); _dataQueue.Enqueue(dataBlock); } // 写入循环 private void WriteQueueLoop() { while (_isRunning || _dataQueue.Count > 0) { if (_dataQueue.TryDequeue(out byte[] data)) { _fileStream.Write(data, 0, data.Length); // 如果用ArrayPool,这里要归还数组:ArrayPool<byte>.Shared.Return(data); } else { // 队列为空时短暂休眠,避免CPU空转 Thread.Sleep(10); } } _fileStream.Flush(); _fileStream.Close(); } // 停止操作 public void Stop() { _isRunning = false; }
额外优化建议
- FileStream参数优化:设置
bufferSize为64KB或更大(比如65536),加上FileOptions.SequentialScan,让系统针对顺序写入做优化 - 使用异步写入:把
_fileStream.Write换成await _fileStream.WriteAsync,进一步降低写入线程的阻塞 - 内存复用:用
ArrayPool<byte>来复用缓冲区数组,减少GC压力,避免内存碎片 - 磁盘性能适配:如果是机械硬盘,30-40MB/s已经接近极限,建议换成SSD;同时关闭实时杀毒软件的文件监控,避免拖慢写入速度
内容的提问来源于stack exchange,提问作者bLAZ
相关产品推荐
相关产品推荐

