Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursion and the await / async Keywords

I have a fragile grasp of how the await keyword works, and I want to extend my understanding of it a bit.

The issue that still makes my head spin is the use of recursion. Here's an example:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;  namespace TestingAwaitOverflow {     class Program     {         static void Main(string[] args)         {             var task = TestAsync(0);             System.Threading.Thread.Sleep(100000);         }          static async Task TestAsync(int count)         {             Console.WriteLine(count);             await TestAsync(count + 1);         }     } } 

This one obviously throws a StackOverflowException.

My understanding is because the code actually runs synchronously until the first asynchronous action, after which it returns a Task object that contains information about the asynchronous operation. In this case, there is no asynchronous operation, thus it just keeps recursing under the false promise that it will eventually get a Task returned.

Now changing it just a tiny bit:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;  namespace TestingAwaitOverflow {     class Program     {         static void Main(string[] args)         {             var task = TestAsync(0);             System.Threading.Thread.Sleep(100000);         }          static async Task TestAsync(int count)         {             await Task.Run(() => Console.WriteLine(count));             await TestAsync(count + 1);         }     } } 

This one does not throw a StackOverflowException. I can sortof see why it works, but I would call it more of a gut feeling (it probably deals with how the code is arranged to use callbacks to avoid building the stack, but I can't translate that gut feeling into an explanation)

So I have two questions:

  • How does the second batch of code avoid a StackOverflowException?
  • Does the second batch of code waste other resources? (for example does it allocate an absurdly large number of Task objects on the heap?)

Thanks!

like image 381
riwalk Avatar asked Dec 10 '12 19:12

riwalk


People also ask

What is the purpose of async await keywords?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete.

What is async and await?

The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains. Async functions may also be defined as expressions.

Are JavaScript recursive functions asynchronous?

The getSentenceFragment function and recursive call to getSentence are now asynchronous, so require the await keyword. And that's it! Note, you may only use await within an async function, so in order to invoke this function, you still need to use promises: getSentence() .

How does await keyword work?

The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. When the asynchronous operation completes, the await operator returns the result of the operation, if any.


1 Answers

The part up to the first await in any function runs synchronously. In the first case it runs into a stack overflow because of that - there is nothing interrupting the function calling itself.

The first await (which does not complete immediately - this is the case for you with high likelyhood) causes the function to return (and to give up its stack space!). It queues the rest of it as a continuation. The TPL ensures that continuations never nest too deeply. If there is a risk of stack overflow the continuation is queued to the thread pool, resetting the stack (which was starting to fill up).

The second example can still overflow! What if the Task.Run task always completed immediately? (This is unlikely but possible with the right OS thread scheduling). Then, the async function would never be interrupted (causing it to return and free all stack space) and the same behavior as in case 1 would result.

like image 117
usr Avatar answered Sep 30 '22 16:09

usr