在C#里处理文件读写时,如果直接围绕FileStream做大量零散的小数据操作,性能往往会非常难看。BufferedStream是System.IO命名空间下的一个装饰器类,它包裹已有的流对象,在内存中维护一个字节数组作为缓冲,从而将多次小写入合并成少量大写入,或者将多次小读取合并成少量大读取,减少底层操作系统的实际IO调用次数。

BufferedStream的基本工作原理
BufferedStream本身并不真正和磁盘打交道,它只是“装饰”了一个真实的流,比如FileStream。当你向BufferedStream写入数据时,数据首先进入内部缓冲区。只有当缓冲区被填满,或者你显式调用Flush、Dispose时,缓冲里的数据才会被转发给底层的流执行真正的写入。读取也是类似:BufferedStream会一次性从底层流读取比用户请求更多的数据放进缓冲,后续读取直接从内存取,避免反复的系统调用。
这种设计的核心价值在于,操作系统级别的IO调用(例如Windows的WriteFile)本身有固定开销,包括用户态到内核态的切换、文件锁获取、磁盘寻道等。如果每一次只写几个字节都走一遍完整流程,CPU会大量浪费在上下文切换上。BufferedStream通过空间换时间,用一块内存缓冲把零散请求聚合成块,使底层流更“舒服”地工作。
如何使用BufferedStream进行文件写入
最常见用法是把FileStream包一层。构造函数的第二个参数可以指定缓冲区大小,不传则使用默认値(通常是4096字节)。下面的例子演示了用BufferedStream写入文本文件,并对比无缓冲的情况。
using System;
using System.IO;
using System.Text;
class Program
{
static void Main()
{
string path = "test_buffer.txt";
// 使用BufferedStream包裹FileStream,缓冲区设为8192字节
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
using (BufferedStream bs = new BufferedStream(fs, 8192))
{
byte[] data = Encoding.UTF8.GetBytes("这是一行日志内容n");
for (int i = 0; i < 10000; i++)
{
bs.Write(data, 0, data.Length);
}
// 退出using时会自动Flush并Dispose
}
Console.WriteLine("写入完成");
}
}
在上面代码中,我们循环写入一万行短文本。如果没有BufferedStream,这一万次Write会直接落到FileStream;有了缓冲后,数据先堆在8192字节的内存块里,满了才真正写盘。你可以把BufferedStream那一层去掉再跑一遍,用Stopwatch计时,差距通常会非常明显。
需要注意的是,BufferedStream的Write方法不会每次都刷盘,所以程序崩溃或断电时缓冲里的数据会丢。如果对安全性要求极高,应在关键节点手动调用bs.Flush()把数据推给底层流,甚至再调用fs.Flush(true)强制操作系统写物理介质。
BufferSize设置有什么讲究
缓冲区大小直接影响性能和资源占用。太小则聚合效果差,太大则浪费内存且增加数据丢失窗口。一般来说,4KB到64KB是常见区间,因为和磁盘扇区、操作系统页缓存粒度比较匹配。下面用表格列出不同设定下的特征:
| 缓冲区大小 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 默认4KB | 内存占用小 | 高频极小写入时仍偏多系统调用 | 通用轻量读写 |
| 8KB-32KB | 较好平衡吞吐与内存 | 略占内存 | 日志、批量导出 |
| 64KB以上 | 极少系统调用,吞吐最高 | 丢数据风险大,占内存 | 临时大文件生成 |
从实测经验看,并不是BufferSize翻倍性能就翻倍。当缓冲已经足以覆盖单次业务写入粒度后,继续加大会出现边际效益骤减。比如一次写一行200字节的日志,8KB缓冲大约能攒40行再刷一次,已经把系统调用降到原来的四十分之一;调到64KB只是降到两百分之一,但内存却多耗了八倍。
另外,如果底层流本身已经带缓冲(例如FileStream在某些框架版本里有内部缓冲),再套一层BufferedStream可能造成双重缓冲,此时应确认是否必要。在.NET Core之后,很多基础流实现已优化,包裹前最好用基准测试验证,而不是盲目加层。
读取场景下的缓冲优化
BufferedStream不仅能写,也能读。比如你需要逐字节分析一个超大文件,直接调用FileStream.Read读一个字节会非常慢,而用BufferedStream读,它会一次性抓一大块进内存,你后续的ReadByte都是从数组取,速度提升显著。
using System;
using System.IO;
class ReaderDemo
{
static void Main()
{
using (FileStream fs = new FileStream("big.bin", FileMode.Open, FileAccess.Read))
using (BufferedStream bs = new BufferedStream(fs, 16384))
{
int b;
long count = 0;
while ((b = bs.ReadByte()) != -1)
{
// 简单统计非0字节
if (b != 0) count++;
}
Console.WriteLine("非0字节数: " + count);
}
}
}
这段代码用ReadByte遍历文件,表面看每次只取一字节,实际BufferedStream在背后每次补足16384字节进缓冲,使效率接近块读取。相比自己维护一个byte数组加FileStream.Read,BufferedStream把细节封装干净,代码更简洁。
不过要强调,如果本来就用Read(byte[] buffer, int offset, int count)按大块读,再套BufferedStream意义就不大,因为你自己已经做了缓冲。BufferedStream最适合的是“被迫小粒度访问”的场合,例如解析器接口只让逐字节读,或者网络协议里按字节判断边界。
常见误区与避坑
第一个误区是以为BufferedStream会自动保证数据不丢。它只是缓冲,不调用Flush前进程意外退出,缓冲数据就没了。第二个误区是缓冲区越大越好,前文已说明边际效应。第三个误区是在using之外长期持有BufferedStream却不刷盘,导致文件看似没内容,其实是都憋在内存里。
经验法则:写日志用8KB到16KB缓冲足够;读解析用16KB到32KB较舒服;任何关键写入后记得Flush;不要对已经块读取的流重复加缓冲。
还有一个细节,BufferedStream的CanSeek属性取决于底层流,如果底层不可寻址(如网络流),BufferedStream也不能Seek。在文件场景下通常没问题,但写通用工具方法时别假设它一定能定位。
小结与代码示例整合
把前面要点揉成一个更完整的工具方法,方便在项目里复用。下面示例同时展示写和读,并显式控制Flush时机。
using System;
using System.IO;
using System.Text;
public static class BufferedFileHelper
{
public static void AppendLines(string path, string[] lines, int bufferSize = 8192)
{
using (var fs = new FileStream(path, FileMode.Append, FileAccess.Write))
using (var bs = new BufferedStream(fs, bufferSize))
{
foreach (var line in lines)
{
var bytes = Encoding.UTF8.GetBytes(line + "n");
bs.Write(bytes, 0, bytes.Length);
}
bs.Flush(); // 显式刷盘,确保写入底层流
}
}
public static int CountNonZero(string path, int bufferSize = 16384)
{
int count = 0;
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
using (var bs = new BufferedStream(fs, bufferSize))
{
int b;
while ((b = bs.ReadByte()) != -1)
{
if (b != 0) count++;
}
}
return count;
}
}
这个帮助类把缓冲区大小参数化,调用方可以按文件用途调整。AppendLines在结束前主动Flush,避免数据滞留在缓冲;CountNonZero利用缓冲做高效逐字节扫描。实际项目中,结合业务IO画像做基准测试,才能把BufferedStream的价值真正落到位。
BufferedStream文件IOBufferSize修改时间:2026-08-07 11:57:43