You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#实现高速二进制数据文件写入,解决外部设备缓冲区堵塞问题

解决高速率二进制数据写入避免设备缓冲区堵塞的方案

核心问题分析

你当前的代码在数据接收事件回调里同步执行文件写入,磁盘IO的速度(尤其是机械盘)很容易跟不上30-40MB/s的接收速率,导致接收线程被阻塞,设备的小缓冲区来不及清空就会溢出丢数据。

最优解决方案:接收与写入解耦

把数据接收(生产者)和文件写入(消费者)分成两个独立的任务/线程,让接收线程只负责快速缓存数据,写入线程专门处理磁盘IO,彻底避免IO阻塞接收流程。下面提供两种易实现的方案:

方案1:双缓冲区(乒乓缓冲)

适合固定块大小的场景,逻辑简单,内存开销可控:

  1. 准备两个大小相同的缓冲区(比如128MB,可根据内存情况调整)
  2. 接收线程往当前活跃的缓冲区写数据,写满后切换到另一个缓冲区,同时通知写入线程处理刚写满的缓冲区
  3. 写入线程写完当前缓冲区后,等待下一个缓冲区的通知

代码示例:

// 定义双缓冲区和线程同步对象
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的生产者-消费者队列

适合数据块大小不固定的场景,灵活性更高:

  1. 用ConcurrentQueue<byte[]>存储接收到的数据块(推荐用内存池复用数组,避免频繁GC)
  2. 接收线程把数据块放入队列
  3. 写入线程不断从队列取出数据块写入文件

代码示例:

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;
}

额外优化建议

  1. FileStream参数优化:设置bufferSize为64KB或更大(比如65536),加上FileOptions.SequentialScan,让系统针对顺序写入做优化
  2. 使用异步写入:把_fileStream.Write换成await _fileStream.WriteAsync,进一步降低写入线程的阻塞
  3. 内存复用:用ArrayPool<byte>来复用缓冲区数组,减少GC压力,避免内存碎片
  4. 磁盘性能适配:如果是机械硬盘,30-40MB/s已经接近极限,建议换成SSD;同时关闭实时杀毒软件的文件监控,避免拖慢写入速度

内容的提问来源于stack exchange,提问作者bLAZ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 04:50:33