Proper way to use DisposeAsync on C# streams
Asked Answered
C

1

16

I'm writing a method which asynchronously writes separate lines of text to a file. If it's cancelled it deletes the created file and jumps out of the loop.

This is the simplified code which works fine... And I marked 2 points which I'm not sure how they are being handled. I want the code to not block the thread in any case.

public async Task<IErrorResult> WriteToFileAsync(string filePath,
                                                 CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    using var stream = new FileStream(filePath, FileMode.Create);
    using var writer = new StreamWriter(stream, Encoding.UTF8);

    foreach (var line in Lines)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            //
            // [1] close, delete and throw if cancelled
            //
            writer.Close();
            stream.Close();
            if (File.Exists(filePath))
                File.Delete(filePath);
            throw new OperationCanceledException();
        }

        // write to the stream
        await writer.WriteLineAsync(line.ToString());
    }

    //
    // [2] flush and let them dispose
    //
    await writer.FlushAsync();
    await stream.FlushAsync();
    // await stream.DisposeAsync(); ??????
    return null;
}

1

I'm calling Close() on FileStream and StreamWriter and I think it will run synchronously and blocks the thread. How can I improve this? I don't want to wait for it to flush the buffer into the file and then delete the file.

2

I suppose the Dispose method will be called and not DisposeAsync at the end of the using scope. (is this assumption correct?).

So Dispose blocks the thread and in order to prevent that I'm flushing first with FlushAsync so that Dispose would perform less things. (to what extent is this true?)

I could also remove using and instead I could write DisposeAsync manually in these two places. But it will decrease readability.

If I open the FileStream with useAsync = true would it automatically call DisposeAsync when using block ends?


Any explanation or a variation of the above code which performs better is appreciated.

Czarism answered 4/11, 2019 at 15:26 Comment(5)
If the Stream class was implementing IAsyncDisposable you could ensure that DisposeAsync would be finally called by await using the stream. But it doesn't implement this interface...Ugly
@TheodorZoulias thanks for the useful info. I didn't know I could do await using :)Czarism
Yeap, it is new C# 8 syntax. :-)Ugly
Doh! I mentioned that the Stream class doesn't implement the IAsyncDisposable based on the documentation. I should look at the Object Explorer. Unfortunately the online documentation is not updated with the new C# 8 features yet.Ugly
@TheodorZoulias yes Stream implements IAsyncDisposable and await using pretty much works. maybe the documentation is outdatedCzarism
K
24

As you have it, the using statement will call Dispose(), not DisposeAsync().

C# 8 brought a new await using syntax, but for some reason it's not mentioned in the What's new in C# 8.0 article.

But it's mentioned elsewhere.

await using var stream = new FileStream(filePath, FileMode.Create);
await using var writer = new StreamWriter(stream, Encoding.UTF8);

But also note that this will only work if:

  • You're using .NET Core 3.0+ since that's when IAsyncDisposable was introduced, or
  • Install the Microsoft.Bcl.AsyncInterfaces NuGet package. Although this only adds the interfaces and doesn't include the versions of the Stream types (FileStream, StreamWriter, etc.) that use it.

Even in the Announcing .NET Core 3.0 article, IAsyncDisposable is only mentioned in passing and never expanded on.

On another note, you don't need to do this (I see why now):

writer.Close();
stream.Close();

Since the documentation for Close says:

This method calls Dispose, specifying true to release all resources. You do not have to specifically call the Close method. Instead, ensure that every Stream object is properly disposed.

Since you're using using, Dispose() (or DisposeAsync()) will be called automatically and Close won't do anything that's not already happening.

So if you do need to specifically close the file, but want to do it asynchronously, just call DisposeAsync() instead. It does the same thing.

await writer.DisposeAsync();

public async Task<IErrorResult> WriteToFileAsync(string filePath,
                                                 CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    await using var stream = new FileStream(filePath, FileMode.Create);
    await using var writer = new StreamWriter(stream, Encoding.UTF8);

    foreach (var line in Lines)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            // not possible to discard, FlushAsync is covered in DisposeAsync
            await writer.DisposeAsync(); // use DisposeAsync instead of Close to not block

            if (File.Exists(filePath))
                File.Delete(filePath);
            throw new OperationCanceledException();
        }

        // write to the stream
        await writer.WriteLineAsync(line.ToString());
    }

    // FlushAsync is covered in DisposeAsync
    return null;
}
Karttikeya answered 4/11, 2019 at 15:54 Comment(13)
Thanks for the info. I Close the streams because I want to immediately delete the file and Dispose occurs too late giving me it is being used by another process exceptionCzarism
Ah, ok. That totally makes sense.Karttikeya
And I can't find a way to just discard what's been written to the buffer so far. So I have to wait for it to flush everything and then throw them away.Czarism
it is not quite true that this is .NET Core 3.0+ - the necessary APIs are available in Microsoft.Bcl.AsyncInterfaces; the problem is that Stream etc doesn't implement this interface yet (before .NET Core 3.0); but in the general sense: the feature can work if components take the ref and implement the APIKirkpatrick
@MarcGravell Thanks! I updated my answer to include that.Karttikeya
await writer.DisposeAsync() works when called manually, however it should be called only for one of the streams since the two streams has one common BaseStream (?). If I call it for both it gives Cannot access a closed file. exception. too much details! :))Czarism
@Czarism Yeah, that makes sense (only disposing one of them).Karttikeya
@Czarism I don't see a way to discard what has been written to the buffer so far without writing it. I looked at the source code and I don't see anything in either FileStream or StreamWriter that will let you do that.Karttikeya
I think I should ask that in another question. Thanks for the help, I added the code I come up with according to your answer.Czarism
It has been asked a couple times before. There are lots of answers there, although they pre-date .NET Core.Karttikeya
By the way, DisposeAsync() will flush the buffer, so you don't need to call FlushAsync() separately.Karttikeya
@GabrielLuci that's true. I reflected that into the answer as wellCzarism
Shouldn't you open the FileStream with useAsync: true? this answer to Why .NET async await file copy is a lot more CPU consuming than synchronous File.Copy() call? as well as docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… suggest to.Fideicommissum

© 2022 - 2024 — McMap. All rights reserved.