14

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.

Bizhan
  • 16,157
  • 9
  • 63
  • 101
  • 1
    If the `Stream` class was implementing [`IAsyncDisposable`](https://learn.microsoft.com/en-us/dotnet/api/system.iasyncdisposable) you could ensure that `DisposeAsync` would be finally called by `await using` the stream. But it doesn't implement this interface... – Theodor Zoulias Nov 04 '19 at 15:41
  • @TheodorZoulias thanks for the useful info. I didn't know I could do `await using` :) – Bizhan Nov 04 '19 at 15:43
  • 1
    Yeap, it is new C# 8 syntax. :-) – Theodor Zoulias Nov 04 '19 at 15:43
  • Doh! I mentioned that the `Stream` class doesn't implement the `IAsyncDisposable` based on the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream?view=netcore-3.0). I should look at the [Object Explorer](https://prnt.sc/psim9n). Unfortunately the online documentation is not updated with the new C# 8 features yet. – Theodor Zoulias Nov 05 '19 at 02:12
  • @TheodorZoulias yes `Stream` implements `IAsyncDisposable` and `await using` pretty much works. maybe the documentation is outdated – Bizhan Nov 05 '19 at 08:25

1 Answers1

22

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;
}
Bizhan
  • 16,157
  • 9
  • 63
  • 101
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • 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` exception – Bizhan Nov 04 '19 at 16:00
  • Ah, ok. That totally makes sense. – Gabriel Luci Nov 04 '19 at 16:03
  • 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. – Bizhan Nov 04 '19 at 16:06
  • 1
    it is not quite true that this is .NET Core 3.0+ - the necessary APIs are available in [`Microsoft.Bcl.AsyncInterfaces`](https://www.nuget.org/packages/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 API – Marc Gravell Nov 04 '19 at 16:12
  • @MarcGravell Thanks! I updated my answer to include that. – Gabriel Luci Nov 04 '19 at 16:18
  • `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! :)) – Bizhan Nov 04 '19 at 16:18
  • @Bizhan Yeah, that makes sense (only disposing one of them). – Gabriel Luci Nov 04 '19 at 16:20
  • 1
    @Bizhan 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](https://github.com/dotnet/corefx/tree/ac99a1b7168bd32046a954c3f06012c0fa909bed/src/Common/src/CoreLib/System/IO) and I don't see anything in either `FileStream` or `StreamWriter` that will let you do that. – Gabriel Luci Nov 04 '19 at 16:29
  • 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. – Bizhan Nov 04 '19 at 16:35
  • It has been asked a [couple](https://stackoverflow.com/questions/7376956/close-a-filestream-without-flush) [times](https://stackoverflow.com/questions/21761947/c-sharp-how-to-clear-the-streamwriter-buffer-without-writing-to-a-txt-file) before. There are lots of answers there, although they pre-date .NET Core. – Gabriel Luci Nov 04 '19 at 16:37
  • 2
    By the way, [`DisposeAsync()` will flush the buffer](https://github.com/dotnet/corefx/blob/ac99a1b7168bd32046a954c3f06012c0fa909bed/src/Common/src/CoreLib/System/IO/FileStream.Windows.cs#L278), so you don't need to call `FlushAsync()` separately. – Gabriel Luci Nov 04 '19 at 16:39
  • @GabrielLuci that's true. I reflected that into the answer as well – Bizhan Nov 04 '19 at 16:42
  • Shouldn't you open the `FileStream` with [`useAsync: true`](https://docs.microsoft.com/en-us/dotnet/api/system.io.filestream.-ctor?view=net-5.0#System_IO_FileStream__ctor_System_String_System_IO_FileMode_System_IO_FileAccess_System_IO_FileShare_System_Int32_System_Boolean_)? [this answer](https://stackoverflow.com/a/42404644) to [Why .NET async await file copy is a lot more CPU consuming than synchronous File.Copy() call?](https://stackoverflow.com/q/42403813) as well as https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/using-async-for-file-access suggest to. – dbc Dec 14 '20 at 21:11