Skip to content

[Breaking change] FileStream.Position is updated AFTER ReadAsync|WriteAsync completes #50858

Closed
@adamsitnik

Description

@adamsitnik

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:

#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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.IObreaking-changeIssue or PR that represents a breaking API or functional change over a prerelease.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions