Asynchronous FileStream Read and Write in C
Handling asynchronous file operations in C# prevents thread blocking
and enhances application scalability, especially when dealing with large
files or high-concurrency environments. This guide demonstrates how to
properly configure a FileStream for non-blocking I/O and
how to read and write data using modern asynchronous methods like
ReadAsync and WriteAsync alongside best
practices such as cancellation tokens and memory management.
Enabling Asynchronous I/O
To achieve true non-blocking I/O at the operating system level, you
must configure the FileStream with the
useAsync parameter set to true, or by using
FileOptions.Asynchronous. Without this flag, calls to
ReadAsync and WriteAsync may execute
synchronously on a thread pool thread rather than utilizing native
asynchronous I/O completion ports.
Using FileStreamOptions is the recommended modern
approach:
var options = new FileStreamOptions
{
Mode = FileMode.OpenOrCreate,
Access = FileAccess.ReadWrite,
Share = FileShare.None,
Options = FileOptions.Asynchronous
};
await using var stream = new FileStream("example.dat", options);Asynchronous Writing with WriteAsync
To write data asynchronously, convert your data into a byte sequence
(such as a byte array or ReadOnlyMemory<byte>) and
call WriteAsync. The use of
ReadOnlyMemory<byte> avoids unnecessary memory
allocations.
using System.Text;
public async Task WriteToFileAsync(string filePath, string text, CancellationToken cancellationToken = default)
{
var options = new FileStreamOptions
{
Mode = FileMode.Create,
Access = FileAccess.Write,
Share = FileShare.None,
Options = FileOptions.Asynchronous
};
byte[] sourceBytes = Encoding.UTF8.GetBytes(text);
ReadOnlyMemory<byte> memoryBuffer = sourceBytes;
await using var fileStream = new FileStream(filePath, options);
// Write data asynchronously
await fileStream.WriteAsync(memoryBuffer, cancellationToken);
// Flush to ensure data is written to the underlying device
await fileStream.FlushAsync(cancellationToken);
}Asynchronous Reading with ReadAsync
Reading a stream asynchronously requires allocating a buffer and
looping until ReadAsync returns 0, which
signals the end of the file (EOF).
using System.Text;
public async Task<string> ReadFromFileAsync(string filePath, CancellationToken cancellationToken = default)
{
var options = new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.Asynchronous
};
await using var fileStream = new FileStream(filePath, options);
// Allocate a buffer (e.g., 4096 bytes)
byte[] buffer = new byte[4096];
Memory<byte> memoryBuffer = buffer;
var stringBuilder = new StringBuilder();
int bytesRead;
// Loop until ReadAsync returns 0 (EOF)
while ((bytesRead = await fileStream.ReadAsync(memoryBuffer, cancellationToken)) > 0)
{
string chunk = Encoding.UTF8.GetString(buffer, 0, bytesRead);
stringBuilder.Append(chunk);
}
return stringBuilder.ToString();
}Copying Between Streams
When transferring data directly from one file or stream to another,
avoid manual buffer loops by using CopyToAsync:
public async Task CopyFilesAsync(string sourcePath, string destinationPath, CancellationToken cancellationToken = default)
{
var sourceOptions = new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Options = FileOptions.Asynchronous | FileOptions.SequentialScan
};
var destinationOptions = new FileStreamOptions
{
Mode = FileMode.Create,
Access = FileAccess.Write,
Options = FileOptions.Asynchronous
};
await using var sourceStream = new FileStream(sourcePath, sourceOptions);
await using var destinationStream = new FileStream(destinationPath, destinationOptions);
await sourceStream.CopyToAsync(destinationStream, bufferSize: 81920, cancellationToken);
}Key Considerations
- Always Use
await using: Ensure theFileStreamis disposed viaIAsyncDisposableto flush buffers and release operating system handles asynchronously. - Pass
CancellationToken: Always pass a cancellation token toReadAsync,WriteAsync, andFlushAsyncto support cancellation requests and avoid hanging operations. - Optimize Buffer Size: For large files, standard buffer sizes between 4,096 and 81,920 bytes provide a good balance between memory usage and throughput.
- Sequential Access: When reading files from start to
finish, specify
FileOptions.SequentialScanto allow the operating system to optimize read-ahead caching.