PipeWriter and PipeReader in C# Pipelines IO
System.IO.Pipelines is a high-performance I/O library in
.NET designed to handle parsing, buffering, and processing data with
minimal memory allocations and overhead. At the core of this
architecture are PipeWriter and PipeReader,
which separate the data production phase from the data consumption
phase. Together, they eliminate the need for manual buffer management,
reduce garbage collection (GC) pressure through automatic memory
pooling, and optimize asynchronous workflows for network sockets, file
operations, and real-time streaming services.
The Purpose of System.IO.Pipelines
Traditional stream-based I/O using System.IO.Stream
often forces developers to allocate byte arrays for reading and writing
data. This approach frequently causes performance bottlenecks due to
continuous array allocations, data copying between internal buffers, and
buffer management complexity when parsing multi-byte messages.
System.IO.Pipelines resolves these issues by using a
decoupled producer-consumer model backed by pooled memory
(MemoryPool<byte>). Instead of forcing a single
stream to handle concurrent reads, writes, and buffer resizes, the
pipeline splits responsibilities between PipeWriter (the
producer) and PipeReader (the consumer).
The Role of PipeWriter
PipeWriter acts as the producer in a pipeline. Its
primary purpose is to provide an efficient, zero-allocation mechanism
for writing data into the underlying pipeline buffers.
Key responsibilities and features of PipeWriter
include:
- Direct Buffer Access: Instead of passing an
allocated array into a stream, you request memory directly from the
pipeline using
GetMemory()orGetSpan(). This allows data to be written directly into memory without intermediate copies. - Commit Control: After writing data to the requested
memory block, you call
Advance(int bytes)to notify the pipeline how many bytes were actually written. - Signaling Availability: Calling
FlushAsync()makes the committed bytes visible to thePipeReaderand wakes up any asynchronous read operations waiting on the other end. - Automatic Backpressure: If a consumer is slower
than a producer,
PipeWritercan pause the writer when internal buffer limits are exceeded, preventing out-of-memory exceptions.
The Role of PipeReader
PipeReader acts as the consumer in a pipeline. Its
primary purpose is to read, parse, and release buffered data safely and
efficiently.
Key responsibilities and features of PipeReader
include:
- Contiguous and Discontiguous Buffer Reading:
ReadAsync()returns aReadResultcontaining aReadOnlySequence<byte>. This structure allows the reader to process data seamlessly across multiple non-contiguous memory chunks without copying them into a single unified array. - Partial Data Parsing: Because network packets often
arrive fragmented,
PipeReaderallows you to inspect data without consuming it immediately. - State and Examination Tracking: Through the
AdvanceTo(SequencePosition consumed, SequencePosition examined)method,PipeReaderlets you mark data as:- Consumed: The pipeline can safely release and reuse this memory for future writes.
- Examined: The pipeline retains this data if it represents an incomplete message, appending incoming data to it until a full frame or message is ready to be parsed.
Why They Work Together
By separating reading and writing into PipeReader and
PipeWriter, .NET enables an asynchronous, concurrent
pipeline where one thread can push raw network bytes into memory while
another parses application-layer messages simultaneously. Memory is
returned to the pool automatically once examined data is marked
consumed, resulting in sustained high throughput, predictable latency,
and near-zero garbage collection overhead.