inwenis
inwenis

Reputation: 409

await does not return to caller as expected using StreamReader.ReadToEndAsync()

Trying to understand streams and async/await. If I undertand correctly await returns execution to the caller so I expected the result of the code below to be:

before calling async ReadToEnd
after the call to ReadToEnd
before calling async ReadToEnd
after the call to ReadToEnd

But insted it is

before calling async ReadToEnd
after the call to ReadToEnd
before calling async ReadToEnd
after await in ReadToEnd. streamReader.BaseStream.GetType(): System.IO.MemoryStream
after the call to ReadToEnd

It seems that using a StreamReader with a FileStream underneath really returns to the caller when calling StreamReader.ReadToEndAsync(), but it does not work when using a StreamReader with a MemoryStream underneath.

Trying to understand what is going on I've read the .NET source code and came to the conculssion that calling StreamReader.ReadToEndAsync() with a MemoryStream underneath eventually calls ReadAsync() (source) on the BaseStream. In case of a MemoryStream the ReadAsync() is just not really a asynhnornous method and thus does not return to the caller.

Is my understanding correct?

using System;
using System.Text;
using System.Linq;
using System.IO;
using System.Threading.Tasks;

static class Program
{
    public static void Main(string[] args)
    {
        var longString = new string(Enumerable.Repeat('x', 100000000).ToArray());
        File.WriteAllText("bigFile.txt", longString, Encoding.UTF32);

        StreamReader fileStreamReader = new StreamReader(File.OpenRead(@"bigFile.txt"));
        Console.WriteLine("before calling async ReadToEnd");
        var task = ReadToEnd(fileStreamReader);
        Console.WriteLine("after the call to ReadToEnd");

        byte[] bytes = Encoding.UTF32.GetBytes(longString);
        StreamReader memoryStreamReader = new StreamReader(new MemoryStream(bytes));
        Console.WriteLine("before calling async ReadToEnd");
        var task2 = ReadToEnd(memoryStreamReader);
        Console.WriteLine("after the call to ReadToEnd");

        fileStreamReader.Dispose();
        memoryStreamReader.Dispose();
    }

    static async Task ReadToEnd(StreamReader streamReader)
    {
        string allText = await streamReader.ReadToEndAsync();
        Console.WriteLine("after await in ReadToEnd. streamReader.BaseStream.GetType(): " + streamReader.BaseStream.GetType());
    }
}

Upvotes: 3

Views: 3845

Answers (2)

Marc Gravell
Marc Gravell

Reputation: 1062530

await only returns execution to the caller if the thing being awaited did not complete synchronously. Yes, async methods can complete synchronously if they choose, either because:

  • they know the answer already (cached results, buffered data, known failure states, etc)
  • they do not have a suitable asynchronous downstream operation to perform at all

MemoryStream is always synchronous, so it always does this.

FileStream, by contrast, may or may not complete synchronously, depending on what data is available in buffers, etc.

The decision as to whether to go back to the caller is driven by GetAwaiter().IsCompleted, which (for Task) comes down to .IsCompleted.

Upvotes: 8

Krzysztof Bracha
Krzysztof Bracha

Reputation: 913

In the first part of your code you are creating StreamReader from File. When you call streamReader.ReadToEndAsync() the file must be read first to memory from the hard drive before it contents can be returned which is done asynchronously.

In the second part of your code you are creating StreamReader from MemoryStream which was built from bytes that have been already loaded in memory of the application. In this case the streamReader.ReadToEndAsync() returns the contents immediately as the data is in memory and can be fetched right away.

Upvotes: 2

Related Questions