Description
FileStream
has never been thread-safe, but until .NET 6 we have tried to somehow support multiple concurrent calls to its async methods (ReadAsync
& WriteAsync
).
We were doing that only on Windows and according to my knowledge, this was not documented anywhere.
In order to allow for 100% asynchronous file IO with FileStream
and fix the following issues:
- FileStream.FlushAsync ends up doing synchronous writes #27643 FileStream.FlushAsync ends up doing synchronous writes
- Win32 FileStream turns async reads into sync reads #16341 Win32 FileStream turns async reads into sync reads
#48813 has introduced locking in the FileStream
buffering logic. Now when buffering is enabled (bufferSize
passed to FileStream
ctor is > 1
) every ReadAsync
& WriteAsync
operation is serialized.
This means, that FileStream.Position
is updated AFTER ReadAsync|WriteAsync completes. Not after the operation is started (the old, windows-specific behavior).
async Task AntiPatternDoNotReuseAsync(string path)
{
byte[] userBuffer = new byte[4_000];
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true); // buffering and async IO!
Task[] writes = new Task[3];
// the first write is buffered, as 4000 < 4096
writes[0] = fs.WriteAsync(userBuffer, 0, userBuffer.Length); // no await
Console.WriteLine(fs.Position); // 4000 for both .NET 5 and .NET 6
// the second write starts the async operation of writing the buffer to the disk as buffer became full
writes[1] = fs.WriteAsync(userBuffer, 0, userBuffer.Length); // no await
// 8000 for .NET 5, for .NET 6 most likely not as the operation has not finished yet and the Position was not updated
Console.WriteLine(fs.Position);
// the third write will most probably wait for the lock to be released
writes[2] = fs.WriteAsync(userBuffer, 0, userBuffer.Length); // no await
// 12000 for .NET 5, for .NET 6 most likely not as the operation has not even started yet and the Position was not updated
Console.WriteLine(fs.Position);
await Task.WhenAll(writes);
Console.WriteLine(fs.Position); // the Position is now up-to-date (12000)
}
Pre-change behavior:
4000
8000
12000
12000
Post-change behavior:
4000
?
?
12000
To enable the .NET 5 behavior, users can specify an AppContext
switch or an environment variable:
{
"configProperties": {
"System.IO.UseNet5CompatFileStream": true
}
}
set DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM=1
@dotnet/compat @stephentoub @jozkee @carlossanlop @jeffhandley