高并发下 FileStream.write 崩溃主因是内核句柄竞争、i/o 阻塞与磁盘寻道放大;streamwriter 非线程安全易致异常或数据错乱;推荐 concurrentqueue + 后台线程批量刷盘解耦生产消费,兼顾性能与稳定性。

为什么直接 FileStream.Write 在高并发下会崩
多个线程同时调用 FileStream.Write 写同一个文件,哪怕加了 lock,也会因频繁的 I/O 阻塞、内核句柄竞争和磁盘寻道放大延迟,吞吐迅速跌穿 1MB/s。更糟的是,StreamWriter 默认带缓冲但非线程安全,跨线程写入可能触发 ObjectDisposedException 或数据错乱。
- windows 上对同一文件的并发
WriteFile调用会被串行化,本质仍是单点瓶颈 - 每次写入都触发一次系统调用,上下文切换成本在万级 QPS 下不可忽略
- 小块写入(如每条日志几十字节)会激增磁盘 I/O 次数,SSD 寿命和延迟双受损
用 ConcurrentQueue + 后台写入线程是最稳的起点
核心思路是解耦“生产”和“消费”:业务线程只往线程安全队列投递数据,由单一后台线程批量刷盘。这避免锁争用,也天然聚合小写请求。
- 用
ConcurrentQueue<string></string>或ConcurrentQueue<byte></byte>存原始数据,比BlockingCollection更轻量(后者含额外同步开销) - 后台线程用
Thread.Sleep(1)或SpinWait.SpinOnce()等待,别用BlockingCollection.Take()—— 它在空时仍会进内核态,增加延迟 - 攒够阈值(如 8KB 或 100 条)再刷盘,或每 10ms 强制 flush 一次,防消息积压过久
var queue = new ConcurrentQueue<byte[]>(); Task.Run(() => { var buffer = new List<byte[]>(128); while (!cancellationToken.IsCancellationRequested) { if (queue.TryDequeue(out var item)) { buffer.Add(item); if (buffer.Sum(b => b.Length) >= 8192) Flush(buffer); } else if (buffer.Count > 0) { Flush(buffer); } else { Thread.Sleep(1); // 低频轮询,避免 CPU 空转 } } });
MemoryMappedFile 适合超大吞吐但代价高
当单机写入持续超过 500MB/s(如金融行情快照),纯队列模型可能因内存拷贝和 GC 压力成为瓶颈。MemoryMappedFile 可绕过 .NET 堆,让多线程直接写共享内存页,再由独立线程调用 FlushViewOfFile 刷盘。
- 必须用
FileOptions.RandomAccess | FileOptions.WriteThrough创建映射,禁用系统缓存,避免 double-buffering - 需自行管理写位置指针(
long类型原子变量),并处理跨页边界——写到页尾要手动切到下一页 - 崩溃恢复难:映射文件不保证落盘顺序,断电可能丢最后几页数据,必须配 CRC 校验和 checkpoint 机制
别忽略 FileOptions.Asynchronous 和 WriteAsync
如果写入目标是网络文件系统(如 SMB 共享)或云存储网关,同步 I/O 会卡死整个线程池。此时应改用异步流,但要注意陷阱:
-
FileStream必须用FileOptions.Asynchronous构造,否则WriteAsync内部仍走同步路径 - 不要在
async void方法里写日志,异常会静默丢失;统一用async Task并 await - 并发
WriteAsync到同一文件仍需锁(如SemaphoreSlim),但锁粒度可缩小到单次 write 调用,而非整条消息
真正难的不是堆砌技术,而是确定你的瓶颈在哪——是磁盘吞吐?CPU 序列化?还是网络延迟?先用 PerfView 抓一下 ThreadPool 和 IO 的热区,再决定上 MemoryMappedFile 还是老实用队列。