3

I am new in async programming and my question might look silly. Can anybody, please, explain how async calls work in the following pseudo code example?

    public async Task MyMethod()
    {
     while(true)
     {
      await Method1();
      //do something in MyMethod 
      await Task.Delay(10000);
     }
    }

    private async Task Method1()
    {
    //do something in Method1 before await
    await Method2();
    //do something in Method1 after await
    }

    private async Task Method2()
    {
    //do something in Method2 before await
    await LongRunMethod();
    }

In my understanding, the program works like this

  1. MyMethod() calls Method1() in infinite loop
  2. Method1() runs "do something in Method1 before await"
  3. Method1() starts Method2() and returns control to MyMethod()
  4. MyMethod() starts 10000 ms delay and returns control to its caller
  5. Method2() completes "do something in Method2 before await", starts LongRunMethod() and returns control to MyMethod1()
  6. Method1() completes "do something in Method1 after await". What method does get control after that?
  7. LongRunMethod() finishes its work
  8. Method2() finishes its work. Does it return control to Method1()?
  9. After 10000ms, everything repeats from step 1.

My questions are

  1. Is the above steps sequence correct?
  2. What methods do get control after Method1() and Method2() finish their work in steps 6 and 8?
  3. What is going to happen if LongRunMethod() runs longer than 10000ms?

Thank you

MBK
  • 525
  • 1
  • 8
  • 23
  • You're never awaiting `_task`. That's asking for trouble. – Kirill Shlenskiy Sep 07 '17 at 22:43
  • Well in JS, is a better practice to `chain` async methods instead of `nesting` them... because on nesting, you loose control... but chaining allows to ensure that your process always runs on the same order. https://stackoverflow.com/questions/25181984/multiple-async-await-chaining – David Espino Sep 07 '17 at 22:48
  • Thank you Kirill, I've changed the code. – MBK Sep 07 '17 at 22:49
  • Actually, this changed the answer to your question 3. Previously if `LongRunMethod` ran longer than 10 seconds, another call would have been started in parallel, whereas now you have nice sequential execution regardless of how long each method takes. Still, every `Task` needs to be `await`ed at some point. – Kirill Shlenskiy Sep 07 '17 at 22:54
  • A more interesting variation of your code would have included `await _task` *after* `await Task.Delay(10000)`. – Kirill Shlenskiy Sep 07 '17 at 22:55
  • Your understanding is wrong starting from step 3. Why not step through the code *in the debugger* and see what the control flow actually is? – Eric Lippert Sep 07 '17 at 23:02

1 Answers1

6

Is the above steps sequence correct?

No.

The correct sequence is

  1. MyMethod calls Method1.
  2. Method1 does the code before the call to Method2.
  3. Method1 calls Method2.
  4. Method2 does the code before the call to LongRunMethod.
  5. Method2 calls LongRunMethod.
  6. LongRunMethod returns a task to Method2.
  7. Method2 interrogates the task to see if it is complete. Let's suppose it is not.
  8. Method2 signs up "do no additional work, and then mark method2's task as complete" as the continuation of the task just returned, and returns its task to Method1.
  9. Method1 interrogates the task just returned from method2. Let's suppose it's not complete. It assigns the work that happens after the await as the continuation of method1's task, and returns that task to its caller.
  10. MyMethod interrogates the task just returned from Method1. Let's suppose it's not complete. It signs up the remainder of that work to the continuation of its task, and returns that task to its caller.
  11. That caller, whatever it is, does whatever work it does.
  12. At some point in the future, LongRunMethod's task completes asynchronously and requests that its continuation be resumed in the appropriate context.
  13. Eventually that context gets to run code, and it runs the continuation of the LongRunMethod task, which is, recall, to do no work and then mark the Method2 task as complete.
  14. It does so. Method2's task is now complete, so it requests its continuation to run on the appropriate context.
  15. Eventually that runs. Recall that Method2 task's continuation is to run the remainder of Method1. It does so, and then marks Method1's task as complete. That requests that Method1's task run its continuation.
  16. Eventually that continuation runs. Its continuation is to call Task.Delay.
  17. Task.Delay returns a task. MyMethod interrogates the task. It's not complete. So it signs up "go back to the top of the loop" as the continuation of that task. It then returns.
  18. Whatever code triggered the continuation of LongRunMethod's task keeps doing work. At some point ten seconds in the future the delay task completes and requests that its continuation executes.
  19. Eventually the continuation executes on the appropriate context. The continuation is "go back to the top of the loop", and the whole thing starts over.

Note that the task returned by MyMethod never completes normally; if any of those tasks throw exceptions then it completes exceptionally. For simplicity I've ignored the checks for exceptional continuation in your example, since there is no exception handling.

What methods do get control after Method1() and Method2() finish their work?

The remainder of Method1 eventually gets control after the task returned by Method2 is completed. MyMethod eventually gets control after the task returned by Method1 is completed. The exact details of when the continuations are scheduled depends on the context in which the code is running; if they're on the UI thread of a form, that's very different than if they're in worker threads on a web server.

What is going to happen if LongRunMethod() runs longer than 10000ms?

I think you mean what happens if the task returned by LongRunMethod does not complete for more than ten seconds. Nothing particularly interesting happens then. The delay is not started until after LongRunMethod's task is done. You awaited that task.

I think you fundamentally do not understand what "await" means. You seem to think that "await" means "start up this code asynchronously", but that is not at all what it means. The code is already asynchronous by assumption. Await manages asynchrony; it does not create it.

Rather, await is an operator on tasks, and it means if this task is complete then keep going; if this task is not complete then sign up the remainder of this method as the continuation of that task and try to find some other work to do on this thread by returning to your caller.

No code that is after an await executes before the awaited task is completed; await means asynchronously wait for the completion of the task. It does NOT mean "start this code asynchronously". Await is for declaring what points in an asynchronous workflow must wait for a task to finish before the workflow can continue. That's why it has "wait" in the name.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Thank you, Eric. This is exactly what I wanted to know. Everything makes sense now. I have a question with regards to the tasks. What does happen with the tasks after they are completed? For example, in my understanding, the new Method1 and Delay tasks are created in each infinite loop iteration. Are they disposed in the end of the iteration? – MBK Sep 08 '17 at 00:08
  • Another question is about your note "the task returned by MyMethod never completes normally". What do you mean by this? What code changes should be done to complete MyMethod task correctly? Thank you. – MBK Sep 08 '17 at 00:16
  • @MBK: You're welcome. Tasks are objects of reference type; they're garbage collected like any other object when there are no more live references and the GC runs. C# 7 allows creation of value-typed tasks which can lower collection pressure in some scenarios at the expense of having the copy-by-value costs associated with value types. My advice: only use value-type tasks when you have a demonstrated performance problem with normal tasks. – Eric Lippert Sep 08 '17 at 00:41
  • @MBK: The task associated with an async method completes normally when that method executes a return; MyMethod never executes a return, so it never completes normally. – Eric Lippert Sep 08 '17 at 00:42
  • @MBK: Similarly, within `void Caller() {int c = MyLoop();} int MyLoop() { while (true) ;}`, `MyLoop` does not return normally, so `c` will never actually be assigned the value of `MyLoop`. Having a return type on a function with an infinite loop tends to be a bit confusing to consumers; having a return type implies that the function can return. – Brian Sep 08 '17 at 13:19
  • @Brian: Some languages have a "noreturn" type distinct from "void". "void" is "this function returns but with no value", and "noreturn" is "this function doesn't even return". Unfortunately C# has no such type. – Eric Lippert Sep 08 '17 at 13:46
  • Thank you so much you, Eric. Your comments are very helpful. – MBK Sep 10 '17 at 22:36