Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I recursively call an async function without overflowing the stack?

Since the return site of the async function isn't the caller, I assume this works, but thought I'd verify that this was safe just in case. If it's not, why would this overflow the stack?

static async Task CheckAsync(TimeSpan recursiveTimer)
{
    // do some work

    await Task.Delay(recursiveTimer);
    CheckAsync(recursiveTimer);
}

Edit: I decided to just try it out - it looks like it does not overflow the stack (it's running on my machine now - it's currently on call 210,000). My assumed reason is that because the return site of the CheckAsync function is not actually CheckAsync, but instead somewhere in the async plumbing. So when CheckAsync calls CheckAsync, it's not actually adding to the call stack via the normal function call mechanism, but instead putting the function as an object on some async "to be executed" queue, which is run via some other thread managing async functions.

To anyone that knows this mechanism well: does this sound about right?

like image 940
Rollie Avatar asked Aug 22 '14 10:08

Rollie


People also ask

How does recursive function prevent stack overflow?

In order to prevent stack overflow bugs, you must have a base case where the function stops make new recursive calls. If there is no base case then the function calls will never stop and eventually a stack overflow will occur. Here is an example of a recursive function with a base case.

Do recursive functions cause stack overflow?

The most-common cause of stack overflow is excessively deep or infinite recursion, in which a function calls itself so many times that the space needed to store the variables and information associated with each call is more than can fit on the stack.

Does recursion always use stack?

Explanation: Recursions are always managed by using stack. 3. Which of these will happen if recursive method does not have a base case? Explanation: If a recursive method does not have a base case then an infinite loop occurs which results in Stack Overflow.

What happens when you call async method without await Javascript?

Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously.


2 Answers

The reason it's working for you is not because of the way CheckAsync is called, but because you're awaiting the result of Task.Delay. That will always return a "not completed yet" task, so awaiting it will schedule a continuation. That continuation will be fired on an effectively empty stack, so it doesn't matter that you then make the recursive call.

Now, I think you still effectively have a memory leak, because IIRC the framework will be keeping track of a "logical stack" which will get bigger and bigger... but that will be stored on the heap and expand until you run out of memory.

If you want to see the stack blow up, all you need to do is change the code to:

static async Task CheckAsync(TimeSpan recursiveTimer)
{
    // Whatever
    await Task.FromResult(5);
    CheckAsync(recursiveTimer);
}

At that point, assuming the code in "whatever" doesn't await anything, you'll have entirely synchronous code, just using Task for keeping track of completion and exceptions.

I certainly wouldn't recommend this as a pattern for repeatedly doing work (partly due to the memory leak I mentioned), but I hope that explains why you're not getting a stack overflow.

like image 128
Jon Skeet Avatar answered Sep 22 '22 03:09

Jon Skeet


Reason why it overflows is because overflows stack.

10 static async Task CheckAsync(TimeSpan recursiveTimer)
20 {
30    // do some work
40    await Task.Delay(recursiveTimer);
50    CheckAsync(recursiveTimer);
60 }

Code execution would go

10 20 30 40 50 60                         //CheckAsync(recursiveTimer)
            10 20 30 40 50 60             //CheckAsync(CheckAsync(recursiveTimer))
                        10 20 30 40 50 60 //CheckAsync(CheckAsync(CheckAsync(recursiveTimer))

Contents of that method should be something like:

while(doCheck){
  await Task.Delay(recursiveTimer);
  CheckAsync(recursiveTimer);
}

Assuming you want to wait after every async call and do this while doCheck is true. I assume that you change doCheck value in another thread.

Take a look at : Best Practices in Asynchronous Programming

like image 22
Margus Avatar answered Sep 22 '22 03:09

Margus