写在前面
Stream 从 .NET 1.0 延续至今,API 随异步模型和内存抽象演进:同步数组重载、TAP 异步、Span<byte> / Memory<byte>,以及面向高吞吐协议处理的 System.IO.Pipelines。这些扩展反映需求演进,不应简单归结为原始设计失败。
把 Stream 的所有坑摊开看,背后其实就是同一个设计张力的不同投影:
Stream 用统一抽象覆盖文件、网络、内存、加密和压缩;高吞吐协议处理还需要池化、多段缓冲区与显式背压,这正是 Pipelines 补充而非全面替代 Stream 的地方。
理解了这层张力,就能看清 .NET 流式 API 二十年的演进:每一层补丁(Span、Memory、ValueTask、Pipelines)都是在重新回答"统一抽象到底要做到什么程度"。本文围绕这条主线把 Stream 从底层机制到选型陷阱彻底讲透。
一、Stream 是什么:抽象基类的雄心与代价
1.1 统一 IO 抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public abstract class Stream : MarshalByRefObject, IAsyncDisposable, IDisposable
{
public abstract bool CanRead { get; }
public abstract bool CanWrite { get; }
public abstract bool CanSeek { get; }
public abstract long Length { get; }
public abstract long Position { get; set; }
public abstract int Read(byte[] buffer, int offset, int count);
public abstract void Write(byte[] buffer, int offset, int count);
public abstract long Seek(long offset, SeekOrigin origin);
public abstract void SetLength(long value);
public abstract void Flush();
}
|
1
2
3
4
5
6
7
8
9
| 设计意图:
让"读文件 / 读网络 / 读内存 / 读加密"用同一套 API
→ 业务代码只依赖 Stream,不依赖具体实现
→ 可以层层包装(装饰器模式)
代价:
→ 抽象被迫提供所有方法,但每个子类都不一定能实现
→ CanRead / CanWrite / CanSeek 标志位遍布 API
→ 不支持的操作直接抛 NotSupportedException
|
1.2 四维能力(实际只有三维常用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 能力 属性 典型实现
─────────────────────────────────────────────────
Read CanRead FileStream / NetworkStream / MemoryStream
Write CanWrite FileStream / NetworkStream / MemoryStream
Seek CanSeek FileStream ✅ / NetworkStream ❌ / MemoryStream ✅
Timeout CanTimeout NetworkStream ✅ / FileStream ❌
CanSeek = false 时:
- Position / Length / SetLength / Seek 都抛 NotSupportedException
- Position 是"游标"概念,对网络流没意义
CanTimeout = true 时:
- ReadTimeout / WriteTimeout 控制读写超时(毫秒)
- 实际效果因实现而异(FileStream 在 Windows 上有 native 重叠 IO)
|
1.3 一个 Stream 可以"什么都不支持"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class NullStream : Stream // 实际叫 Stream.Null
{
public override bool CanRead => true;
public override bool CanWrite => true;
public override bool CanSeek => true;
public override long Length => 0;
public override long Position { get; set; }
public override int Read(byte[] buf, int off, int count) => 0; // EOF
public override void Write(byte[] buf, int off, int count) { /* 丢弃 */ }
public override long Seek(long off, SeekOrigin org) => off;
public override void Flush() { }
public override void SetLength(long v) { }
}
|
Stream.Null 是"黑洞"——读立刻 EOF,写什么都不存。这种"无所不接"的抽象,让 Stream 可以做单元测试替身。
二、最坑的"暗契约":Read 不保证读满
这是新手最容易踩的雷,也是 Stream 抽象的"原罪"。
2.1 反模式:以为一次 Read 能读满
1
2
3
4
5
6
7
| // ❌ 高风险写法
public async Task<byte[]> ReadNAsync(Stream s, int n)
{
byte[] buf = new byte[n];
int read = await s.ReadAsync(buf, 0, n); // ⚠️ read 可能 < n
return buf; // ⚠️ 后半段可能是 0
}
|
1
2
3
4
5
6
7
8
9
10
11
| 为什么 Read 可能返回少于请求?
- FileStream:通常能读满(除非到 EOF)
- NetworkStream:可能 1 字节就返回!
* TCP 是字节流,每次 Read 看到的是"当前缓冲区到达的数据"
* 没有"凑够 N 字节"的概念
* 如果对方写 4 字节后 sleep,下次 Read 就只有 4 字节
- CryptoStream:可能解一帧就返回
- GZipStream:可能解一 chunk 就返回
→ 契约:Read 返回"实际读到的字节数",可能是 0(EOF)到 count
|
2.2 正确做法:循环到 N 或 EOF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public static async Task<int> ReadExactAsync(Stream s, byte[] buf, int offset, int count)
{
int total = 0;
while (total < count)
{
int read = await s.ReadAsync(buf, offset + total, count - total);
if (read == 0) break; // EOF
total += read;
}
return total;
}
// .NET 7+ 已经原生提供;异步重载可接收 CancellationToken:
await s.ReadExactlyAsync(buf); // 读满,否则抛 EndOfStreamException
await s.ReadAtLeastAsync(buf, 100); // 至少 100 字节
|
1
2
3
4
5
6
7
| ReadExactly 的实现:
.NET 7 引入的便利 API
替代手写循环(手写循环是 .NET 头号反模式之一)
注意:ReadExactly 在网络流上"卡住"直到读满或 EOF
→ 必须配合 CancellationToken
→ 否则恶意对端可以让调用永久阻塞
|
2.3 Read 返回 0 的意义
1
2
3
4
5
6
7
8
| 对非空缓冲区:0 通常表示 EOF(流结束)
1~count = 实际读到
异常 = IO 错误
注意:
- NetworkStream 上对端关连接 → Read 返回 0
- 永远不会"Read 返回 0 然后又读到数据"
- 收到 0 后再调 Read 还是 0
|
三、API 演进史:从 byte[] 到 PipeReader
每一代 .NET 都在补 Stream 的洞。
3.1 第一代:同步 byte[] API(.NET 1.0)
1
2
3
| byte[] buf = new byte[1024];
int read = stream.Read(buf, 0, 1024);
stream.Write(buf, 0, read);
|
1
2
3
4
| 问题:
- 同步阻塞 → 高并发下线程爆炸
- byte[] 必须分配 → GC 压力
- offset + count 三参数 → 容易越界
|
3.2 第二代:APM 异步(.NET 1.1+)
1
2
| stream.BeginRead(buf, 0, 1024, callback, state);
// 在 callback 里调 EndRead 拿到字节数
|
1
2
3
4
| 问题:
- 回调地狱
- 状态对象装箱
- 异常处理复杂
|
3.3 第三代:TAP 异步(.NET 4.5)
1
| int read = await stream.ReadAsync(buf, 0, 1024);
|
1
2
3
| 问题:
- 仍然 byte[] 分配
- Task<int> 装箱(每个 await 一个 Task 对象)
|
3.4 第四代:Span / Memory(.NET Core 2.1+)
1
2
3
4
5
6
7
8
9
| // 同步版本
int read = stream.Read(buf.AsSpan(0, 1024));
// 异步版本(基于 ValueTask)
int read = await stream.ReadAsync(buf.AsMemory(0, 1024));
// stackalloc 友好
Span<byte> stackBuf = stackalloc byte[256];
stream.Read(stackBuf);
|
1
2
3
4
5
6
7
8
| 改进:
- Span 零分配
- ValueTask<int> 避免每次 await 创建 Task
- 可以用 stackalloc
遗留:
- 仍然要"主动拉"(pull)
- 仍然要循环到 N 字节
|
3.5 第五代:ReadExactly / ReadAtLeast(.NET 7+)
1
2
3
4
5
6
7
| static async Task ReadFrameAsync(
Stream stream, Memory<byte> buffer, CancellationToken ct)
{
await stream.ReadExactlyAsync(buffer, ct);
await stream.ReadAtLeastAsync(
buffer, 100, throwOnEndOfStream: false, cancellationToken: ct);
}
|
3.6 第六代:Pipelines(.NET Core 2.1+,命名空间 System.IO.Pipelines)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| PipeReader reader = PipeReader.Create(stream);
try
{
while (true)
{
ReadResult result = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
SequencePosition consumed = ProcessProtocol(buffer);
reader.AdvanceTo(consumed, buffer.End);
if (result.IsCompleted && buffer.Slice(consumed).IsEmpty)
break;
}
}
finally
{
await reader.CompleteAsync();
}
|
1
2
3
4
5
6
7
8
| 革命性:
1. 不用自己分配 buffer(Pipe 内部池化)
2. Pipe 管理缓冲区;协议解析器仍需循环并保留不完整帧
3. 多段连续内存(ReadOnlySequence<byte>)
4. 背压(Pipe 满了写不动)
5. 减少应用层不必要的复制(不代表整个 I/O 路径零拷贝)
ASP.NET Core / Kestrel / SignalR 全部用 Pipelines
|
四、同步 vs 异步:按工作负载选择 FileStream 选项
这是另一个高频坑。
4.1 默认句柄按同步方式打开
1
2
3
4
| // 看起来是异步
using var fs = new FileStream("data.bin", FileMode.Open);
byte[] buf = new byte[4096];
int read = await fs.ReadAsync(buf, 0, 4096);
|
1
2
3
4
5
6
7
8
9
| 实际行为:
- 默认构造函数没有设置 `FileOptions.Asynchronous`
- 异步重载仍可调用,但具体实现与成本因操作系统和运行时版本而异
- 不要用“假异步”概括所有平台
为什么?
- Windows 文件 IO 默认是同步的
- 要真正异步需要 FILE_FLAG_OVERLAPPED + 完成端口
- 是否值得异步打开,要看并发度、访问模式和平台
|
4.2 真异步:FileOptions.Asynchronous
1
2
3
4
5
6
7
8
9
10
11
12
| // ✅ 真异步
using var fs = new FileStream(
"data.bin",
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
options: FileOptions.Asynchronous // ← 关键
);
int read = await fs.ReadAsync(buf, 0, 4096);
// Windows 可使用 overlapped I/O + IOCP;Unix 实现由运行时决定
|
1
2
3
4
5
6
7
8
9
10
| FileOptions.Asynchronous 的效果:
- Windows:内部用 OVERLAPPED + IOCP
- Linux/Unix:不能笼统声称使用 io_uring;.NET 运行时的 io_uring 支持长期仍是独立规划项
- 实际线程占用和收益依平台、文件系统与运行时实现而变
FileSteam 静态方法:
File.OpenRead("...") // 没开 Asynchronous
File.Open("...", FileMode) // 没开
→ 需要异步句柄语义时,显式使用 `FileOptions.Asynchronous` 或 `FileStreamOptions.Options`
|
4.3 性能对比
1
| 不存在脱离磁盘、文件系统、平台和并发模型的固定“6 倍”结果。请分别基准同步顺序 I/O、异步并发 I/O,并同时观察吞吐、延迟、线程池队列和 CPU。
|
4.4 同步 ReadAsync 反而慢
1
2
3
4
5
6
7
8
9
10
11
| // 真异步下,单线程顺序读:
await fs.ReadAsync(buf);
await fs.ReadAsync(buf);
await fs.ReadAsync(buf);
// 比同步:
fs.Read(buf);
fs.Read(buf);
fs.Read(buf);
// 慢一点(每次有 IOCP 唤醒开销)
// 并发 I/O 是否受益需要在目标平台测量
|
五、装饰器模式:Stream 的灵魂
Stream 是 GoF《设计模式》里 Decorator(装饰器)的教科书案例。
5.1 经典装饰器组合
1
2
3
4
5
6
7
8
9
10
| // 三层包装:加密 + 压缩 + 文件
using var fs = new FileStream("data.gz.enc", FileMode.Create);
using var cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write);
using var gz = new GZipStream(cs, CompressionMode.Compress);
byte[] data = Encoding.UTF8.GetBytes("Hello, World!");
gz.Write(data, 0, data.Length);
// 数据流:
// Write → gz 压缩 → cs 加密 → fs 落盘
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 图示:
┌─────────────────────┐
│ GZipStream │
│ - 压缩 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ CryptoStream │
│ - 加密 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ BufferedStream │
│ - 缓冲 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ FileStream │
│ - 落盘 │
└─────────────────────┘
|
5.2 装饰器的"自动 dispose 链"
1
2
3
4
5
6
7
| using var fs = new FileStream(...);
using var gz = new GZipStream(fs, ...);
// gz.Dispose() 内部会调用 fs.Dispose()
// 所以只 using 外层就够
// 但很多人不知道这点
// 容易在 finally 里手动 dispose 所有层
|
5.3 装饰器的所有权陷阱
1
2
3
4
5
6
7
8
9
10
11
12
| // 假设你有一个"共享底层流"
using var fs = new FileStream(...);
// 包装两个装饰器,都接管 fs
using var cs1 = new CryptoStream(fs, enc1, Write);
using var cs2 = new CryptoStream(fs, enc2, Write);
cs1.Write(...); // 内部调 fs.Write
cs2.Write(...); // 也调 fs.Write!
// 实际写入是混乱的(fs 是有状态的)
// 正确做法:每个底层流只对应一个装饰器链
|
5.4 BufferedStream:什么时候有用
1
2
3
4
5
6
7
8
9
| using var fs = new FileStream(...);
using var bs = new BufferedStream(fs, 8192); // 8KB 缓冲
// FileStream 是否启用托管缓冲取决于构造参数和运行时实现
// NetworkStream 自身不增加托管缓冲;Socket 缓冲大小由平台和配置决定
// 是否再包 BufferedStream 应按读写粒度和目标 Stream 实测
// 对 CryptoStream / GZipStream 的大量小读写,外层缓冲可能减少下游调用
// 但装饰器顺序会改变语义,仍应按完整链路验证
|
六、CopyTo / CopyToAsync:被低估的 API
6.1 自实现 vs 内置
1
2
3
4
5
6
7
8
9
10
11
| // ❌ 手写循环
public async Task CopyAsync(Stream src, Stream dst)
{
byte[] buf = new byte[8192];
int read;
while ((read = await src.ReadAsync(buf, 0, 8192)) > 0)
await dst.WriteAsync(buf, 0, read);
}
// ✅ 内置(已经做了优化)
await src.CopyToAsync(dst, bufferSize: 8192);
|
6.2 CopyTo 的实现细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 简化版
public virtual async Task CopyToAsync(Stream dest, int bufferSize, CancellationToken ct)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int read;
while ((read = await ReadAsync(buffer, ct)) > 0)
await dest.WriteAsync(buffer.AsMemory(0, read), ct);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
|
1
2
3
4
5
| 内置的优势:
- 基础实现复用池化缓冲区,降低每次调用的数组分配;异步状态和具体子类仍可能分配
- 自适应 bufferSize
- Stream 重写时有优化路径(如 MemoryStream → MemoryStream 直接拷贝)
- 取消支持
|
6.3 FileStream → FileStream 的最优写法
1
2
3
4
5
| // 通用且正确;具体 Stream 子类可以重写 CopyToAsync 优化
await src.CopyToAsync(dst);
// 不要假设 FileStream → FileStream 会自动改用 Windows CopyFileEx;
// 需要操作系统文件复制语义时,应直接选择相应文件 API 并验证元数据/覆盖行为。
|
七、Pipelines:Stream 的接班人
7.1 为什么需要 Pipelines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Stream 抽象的根本问题:
1. 应用必须分配 buffer(byte[])
2. 应用必须循环 Read
3. 应用必须处理"读不满"的情况
4. 没有统一的高低水位背压协议;不同 Stream 的阻塞/缓冲行为各异
5. 单段 buffer(数据可能跨段)
→ 写一个高性能 HTTP 服务器,光是处理"读满 N 字节"就要 100 行代码
Pipelines 重新设计:
1. Pipe 内部管理 buffer(池化)
2. Pipe 自动累积数据
3. 调用方一次拿到"已经够多"的数据
4. Pipe 满了写不动(背压)
5. ReadOnlySequence<byte> 支持多段
|
7.2 PipeReader / PipeWriter
1
2
3
4
5
6
7
8
9
| var pipe = new Pipe(new PipeOptions(
pool: ArrayPool<byte>.Shared,
readerScheduler: PipeScheduler.ThreadPool,
writerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: 1024 * 1024, // 写暂停阈值(背压)
resumeWriterThreshold: 512 * 1024)); // 写恢复阈值
PipeWriter writer = pipe.Writer;
PipeReader reader = pipe.Reader;
|
7.3 协议解析示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 解析"4 字节长度 + N 字节 body"协议
async Task ProcessAsync(PipeReader reader)
{
while (true)
{
ReadResult result = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
while (TryReadMessage(ref buffer, out var message))
{
ProcessMessage(message);
}
// 告诉 reader:消费到哪,下次从哪开始
reader.AdvanceTo(buffer.Start, buffer.End);
if (result.IsCompleted) break;
}
}
bool TryReadMessage(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> message)
{
if (buffer.Length < 4) { message = default; return false; }
Span<byte> lengthBytes = stackalloc byte[4];
buffer.Slice(0, 4).CopyTo(lengthBytes);
int length = BitConverter.ToInt32(lengthBytes);
if (buffer.Length < 4 + length) { message = default; return false; }
message = buffer.Slice(4, length);
buffer = buffer.Slice(4 + length);
return true;
}
|
1
2
3
4
5
| 对比 Stream 版本:
- 没有手动 buffer 分配
- 没有手动"读满 4 字节"循环
- 处理"半个消息"自动等下次数据
- 多段 Sequence 天然支持
|
7.4 Stream 与 Pipe 的桥接
1
2
3
4
5
6
7
8
9
10
11
| // 从 Stream 创建 PipeReader
PipeReader reader = PipeReader.Create(stream, new StreamPipeReaderOptions(
bufferSize: 4096,
leaveOpen: false));
// 从 Stream 创建 PipeWriter
PipeWriter writer = PipeWriter.Create(stream, new StreamPipeWriterOptions(
leaveOpen: false));
// 或者反过来:把 Pipe 当 Stream 用
Stream stream = pipe.Writer.AsStream();
|
1
2
3
4
5
| ASP.NET Core 的请求体就是 PipeReader:
HttpContext.Request.BodyReader : PipeReader
HttpContext.Response.BodyWriter : PipeWriter
→ 不再是 Stream,而是 Pipeline
|
八、常见误用清单
8.1 不 Dispose 流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // ❌ 流泄漏
public byte[] Read(string path)
{
var fs = new FileStream(path, FileMode.Open);
var ms = new MemoryStream();
fs.CopyTo(ms);
return ms.ToArray(); // fs 和 ms 都没 Dispose
}
// ✅ using
public byte[] Read(string path)
{
using var fs = new FileStream(path, FileMode.Open);
using var ms = new MemoryStream();
fs.CopyTo(ms);
return ms.ToArray();
}
|
8.2 异步方法不 await
1
2
3
4
5
6
7
8
9
10
11
12
| // ❌ 火并忘记(fire and forget)
public void BadWrite(Stream s, byte[] data)
{
s.WriteAsync(data, 0, data.Length); // ⚠️ 没 await
// 方法立即返回,写还没完成
}
// ✅ await 或显式处理
public async Task WriteAsync(Stream s, byte[] data)
{
await s.WriteAsync(data);
}
|
8.3 不理解 Asynchronous 的平台差异
1
2
3
4
5
6
7
8
| // 默认句柄按同步方式打开;异步重载的实现成本依平台而异
using var fs = File.OpenRead("data.bin"); // 默认 options
await fs.ReadAsync(buf); // 实际是线程池阻塞
// ✅ 真异步
using var fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read,
FileShare.Read, 4096, FileOptions.Asynchronous);
await fs.ReadAsync(buf);
|
8.4 不用 ArrayPool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // ❌ 每次 new
public async Task<byte[]> ReadAsync(Stream s)
{
byte[] buf = new byte[4096]; // ⚠️ GC 压力
using var ms = new MemoryStream();
int read;
while ((read = await s.ReadAsync(buf)) > 0)
ms.Write(buf, 0, read);
return ms.ToArray();
}
// ✅ ArrayPool
public async Task<byte[]> ReadAsync(Stream s)
{
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
try
{
using var ms = new MemoryStream();
int read;
while ((read = await s.ReadAsync(buf.AsMemory(0, 4096))) > 0)
ms.Write(buf, 0, read);
return ms.ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
// ✅✅ CopyToAsync(最简)
public async Task<byte[]> ReadAsync(Stream s)
{
using var ms = new MemoryStream();
await s.CopyToAsync(ms);
return ms.ToArray();
}
|
8.5 信任 CanSeek
1
2
3
4
5
6
7
8
9
| // ❌ 假设 stream 可 Seek
stream.Position = 0;
stream.Read(...);
// ✅ 检查
if (stream.CanSeek)
stream.Position = 0;
else
throw new InvalidOperationException("Stream is not seekable");
|
8.6 CryptoStream 不 FlushFinalBlock
1
2
3
4
5
6
7
8
9
10
| // ❌ 加密数据不完整
using var cs = new CryptoStream(ms, enc, CryptoStreamMode.Write);
cs.Write(data, 0, data.Length);
// 没有 cs.FlushFinalBlock() 或 Dispose → AES 填充没写
// ✅ Dispose 自动 flush
using (var cs = new CryptoStream(ms, enc, CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
} // ← Dispose 触发 FlushFinalBlock
|
九、性能基准
9.1 Read 模式对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| [MemoryDiagnoser]
public class StreamBench
{
private MemoryStream _stream;
[GlobalSetup]
public void Setup()
{
var data = new byte[1_000_000];
new Random().NextBytes(data);
_stream = new MemoryStream(data);
}
[Benchmark]
public int ReadAll_Array()
{
_stream.Position = 0;
byte[] buf = new byte[4096];
int total = 0;
int read;
while ((read = _stream.Read(buf, 0, buf.Length)) > 0) total += read;
return total;
}
[Benchmark]
public async Task<int> ReadAllAsync_Array()
{
_stream.Position = 0;
byte[] buf = new byte[4096];
int total = 0;
int read;
while ((read = await _stream.ReadAsync(buf, 0, buf.Length)) > 0) total += read;
return total;
}
[Benchmark]
public async Task<int> ReadAllAsync_Pooled()
{
_stream.Position = 0;
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
try
{
int total = 0;
int read;
while ((read = await _stream.ReadAsync(buf.AsMemory(0, 4096))) > 0) total += read;
return total;
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
[Benchmark]
public async Task<int> ReadAll_CopyToAsync()
{
_stream.Position = 0;
using var ms = new MemoryStream();
await _stream.CopyToAsync(ms, 4096);
return (int)ms.Length;
}
}
|
1
| 这段基准使用 `MemoryStream`,主要测量 API、状态机和复制开销,不能代表磁盘或网络 I/O。不要在缺少 CPU、运行时版本、完整 BenchmarkDotNet 报告和误差时给出“典型结果”;应在目标 Stream 类型和并发模型上重新测量。
|
十、Stream 子类速查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| 按用途分类:
文件:
FileStream - 文件 IO(默认 buffered)
FileStream(FileOptions.Asynchronous) - 真异步
UnmanagedMemoryStream - 内存映射文件 / 非托管内存
网络:
NetworkStream - Socket 包装
SslStream - SSL/TLS 加密层
AuthenticatedStream - 基类,可继承做认证
内存:
MemoryStream - byte[] 包装
ReadOnlyMemory<byte> 可通过自定义 Stream 或第三方适配器暴露;BCL 没有 ReadOnlyMemoryStream 类型
RecyclableMemoryStream - Microsoft.IO.RecyclableMemoryStream 库,池化版
管道:
AnonymousPipeServerStream / AnonymousPipeClientStream - 父子进程
NamedPipeServerStream / NamedPipeClientStream - 命名管道
装饰器:
BufferedStream - 缓冲
CryptoStream - 加密
GZipStream / DeflateStream / BrotliStream - 压缩
其他:
Stream.Null - 黑洞
IsolatedStorageFileStream - 沙盒存储
|
十一、选型决策
11.1 读文件
1
2
3
4
5
6
7
8
9
10
11
12
| 默认:
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read,
FileShare.Read, 4096, FileOptions.Asynchronous);
→ 并发异步读取时评估 Asynchronous;单个顺序文件不一定更快
→ 简单一次性读取:File.ReadAllBytesAsync(内部就是上面的封装)
需要 seek:
✅ FileStream 默认支持
需要内存映射:
MemoryMappedFile.CreateFromFile(...)
|
11.2 写文件
1
2
3
4
5
6
7
8
9
| 默认:
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write,
FileShare.None, 4096, FileOptions.Asynchronous);
→ 加 BufferedStream 收益不大(FileStream 已有 buffer)
→ FileOptions.WriteThrough 用于关键数据(绕过 OS 缓存,直接落盘)
追加:
File.AppendAllTextAsync / AppendAllBytesAsync
|
11.3 网络 IO
1
2
3
4
5
6
| 服务器:
Socket → NetworkStream → 用 PipeReader 包装
→ 高性能场景直接用 Pipelines API
客户端:
HTTP 客户端优先使用 HttpClient;只有实现自定义 TCP 协议时才直接使用 Socket/NetworkStream
|
11.4 压缩 + 加密
1
2
3
| 写(从应用到文件):GZipStream → CryptoStream → FileStream
读(从文件到应用):FileStream → CryptoStream → GZipStream
注意 dispose 顺序(外层先 dispose,自动级联)
|
十二、小结
本文讲了 Stream 抽象从 .NET 1.0 到 9 的演进:
- Stream 的"统一抽象"雄心:用基类覆盖所有 IO,代价是 CanXxx 标志遍布
- 最大的暗契约:Read 不保证读满,循环到 N 字节是基本功
- API 六代演进:同步 → APM → TAP → Span/Memory → ReadExactly → Pipelines
- FileStream 的异步语义依平台和打开选项而变;并发异步场景可显式评估
FileOptions.Asynchronous - 装饰器模式:GZipStream(CryptoStream(FileStream)) 的级联与 dispose
- CopyToAsync 内部已经用 ArrayPool + Span 优化,优先用
- Pipelines 是 Stream 在高吞吐协议处理场景的补充:池化 buffer + 背压 + 多段 Sequence
- ASP.NET Core 内部已经从 Stream 迁移到 PipeReader/Writer
- 6 类常见误用:不 Dispose / 不 await / 不开 Async / 不用 Pool / 信任 CanSeek / 不 Flush
- 子类速查与选型决策
1
2
3
4
| 记住三句话:
1. Stream 是"拉"模式 + "同步"基础,Pipelines 是"推"模式 + 背压原生
2. FileStream 是否使用 Asynchronous 要按平台、并发度和访问模式测量
3. Stream.Read 返回值永远不能假设"读满"——这是抽象的代价
|
写文件或网络代码时,先保证短读、取消、释放和所有权正确,再依据 profiling 选择 FileOptions.Asynchronous、池化缓冲区、CopyToAsync 或 Pipelines;不要把这些选项机械叠加。
参考资料