-1

I am trying to understand what exactly, in C#, a method using async await is doing under the hood.

Consider the following two examples:

static async Task<byte[]> MyReadAsync1(Stream source)
{
    var result = new byte[1024];
    int readBytes = 0;

    while (readBytes != result.Length)
    {
        int singleRead = await source.ReadAsync(result, readBytes, result.Length - readBytes);
        if (singleRead == 0)
            break;
        readBytes += singleRead;
    }

    if (readBytes == result.Length)
        return result;
    return result.Take(readBytes).ToArray();
}

static Task<byte[]> MyReadAsync2(Stream source)
{
    return Task.Run(() =>
    {
        var result = new byte[1024];
        int readBytes = 0;

        while (readBytes != result.Length)
        {
            int singleRead;
            try
            {
                singleRead = source.ReadAsync(result, readBytes, result.Length - readBytes).Result;
            }
            catch (AggregateException e)
            {
                ExceptionDispatchInfo.Capture(e.InnerException).Throw();
            }

            if (singleRead == 0)
                break;
            readBytes += singleRead;
        }

        if (readBytes == result.Length)
            return result;
        return result.Take(readBytes).ToArray();
    });
}

My question is: Are these two methods equivalent?

If I execute them, it looks like they are. Neither of them is blocking the calling thread. They are doing the exact same thing as far as I can tell, even in case of any exceptions being thrown.

If they really are equivalent, does the compiler produce the same code? Would that mean that async await does nothing else but save me type some boilerplate code, starting a task and unwrapping AggregateExceptions?

If there is a difference, what exactly is it and would it be possible to achieve the exact same behavior without async await?

Note: I've seen the question What is the purpose of “return await” in C#? and I understand the answers given there. But that covers only the case of one single return statement, whereas my question is meant to be more general.

I'm not questioning the purpose of async await, I just want to understand it in detail.

Update

The possible duplicate does not answer my question. That question only covers a one single line of code, comparing .Result and await. In my question I have many lines of additional code that are "mixed" with asynchronous code and in MyReadAsync2 method I'm wrapping the entire execution in Task.Run. I cannot see how the other question addresses any of these additional questions.

sebrockm
  • 5,733
  • 2
  • 16
  • 39
  • 2
    Since you shouldn't be using `.Result` anyway I'd say it's a moot point. And no, they're not equivalent, the async/await method is split into two, the code before and up to the async part, and the code after, it isn't a simple block and a call to .Result. – Lasse V. Karlsen Jun 15 '18 at 10:18
  • 2
    Possible duplicate of [What is the difference between await Task and Task.Result?](https://stackoverflow.com/questions/27464287/what-is-the-difference-between-await-taskt-and-taskt-result) – Camilo Terevinto Jun 15 '18 at 10:20
  • 1
    "Neither of them is blocking the calling thread" - no, but one of them is *creating* a task - this will require one of the threads from the thread pool to run on and *that* thread will be blocked. Option 1 blocks _zero_ threads, option 2 blocks one (just not the calling one) – Damien_The_Unbeliever Jun 15 '18 at 10:26
  • @Damien_The_Unbeliever Thank you! This is precisely the kind of answer that I am looking for. Are there any other of these differences? – sebrockm Jun 15 '18 at 10:31
  • For some more insight, you may also want to read Stephen Cleary's [There is no thread](https://blog.stephencleary.com/2013/11/there-is-no-thread.html) – Damien_The_Unbeliever Jun 15 '18 at 10:32
  • @CamiloTerevinto I have updated my question. Your possible duplicate only explains the differences between `.Result` and `await` which is also interresting, but, if at all, only scratches partially what I have asked. I want to know how to write an `async await` free equivalent (as title says). Or, if that's impossible, I would like to know *why*. – sebrockm Jun 15 '18 at 10:41
  • @sebrockm I vaguely remember Jon Skeet's C# in Depth talks about how async await works under the hood. Maybe check that out? – Sweeper Jun 15 '18 at 10:46
  • @LasseVågsætherKarlsen sorry, I've somehow realized your comment just now. This split also sound quite like what I've been asking for. Could you explain that in a little more detail or show me where that is explained? Thanks. – sebrockm Jun 15 '18 at 10:56
  • In addition, blocking a single (pool) thread is a very tiny problem. It only matters if you do that many times, ie on a server. – bommelding Jun 15 '18 at 11:25
  • @bommelding - it can matter if you decide that this is how you're going to write "async" code *throughout* a codebase. – Damien_The_Unbeliever Jun 15 '18 at 12:08
  • If that code base runs on a client I still wouldn't care much. If you do it in only one place on a Server, I would pay attention. (I know you can come up with a bad client side scenario but that would be artificial or very rare). – bommelding Jun 15 '18 at 12:32
  • @CamiloTerevinto could you please checkout the answer that I accepted and compare it with the answers in your "possible duplicate"? After doing that could you please realize that my question goes beyond the other one and thus hardly can be considered a duplicate? Could you than please remove that "This question may already have an answer here"-tag, or alternatively find some question that really is a duplicate of mine? Thank you! :) – sebrockm Jun 16 '18 at 14:58

1 Answers1

1

Roughly speaking, if you wanted to write an "async free" version of the code, you'd be using ContinueWith in about the same location as each await, rather than using Result (which as discussed, will block a thread and may result in deadlocks).

Of course, this ignores the synchronization context machinery which e.g. can get your code running back on the UI thread if that was the context in which the original method was called. You'll also find (if you have more than a single await) that your nesting levels go crazy and that you can't easily use some constructs (e.g. try doing an await inside a loop, and then try to write the equivalent using ContinueWith1,2).

There's a lot of machinery that async/await just gives you and you don't often have to peek behind the scenes. If you want to though, you can always try compiling some async based code and then running it through a decompiler that either doesn't support async or has an option to not try to reverse-engineer async methods.


1Not that simple loops just calling await is a good typical example of async code in most cases.

2Note that in this example that each iteration of the loop only starts when the previous awaitable has completed - so you have to pass the loop context into whatever method you're passing to the ContinueWith call.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • Thanks for your answer! I will accept it, thought it doesn't provide any code which would have been optimal. At least you gave me some important insight, e.g. that it rather translates to `ContinueWith` rather than to `Result`. I don't find that in the oh so identical "possible duplicate". – sebrockm Jun 16 '18 at 14:53