Just facing this curious case, and I wonder whether this pattern will yield any guarantee on the stack usage (i.e. is reliable or not).
NOTE: the counter is just an hypothetical work to do in the "DoIt" function.
class Program
{
static void Main(string[] args)
{
DoIt();
Console.ReadKey();
_flag = true;
}
private static bool _flag;
private static int _count;
private static async void DoIt()
{
await Task.Delay(1000);
_count++;
if (_flag) return;
DoIt();
}
}
The "DoIt" task should call itself forever, until something (i.e. the flag) breaks the recursion. My wonder is whether the stacks fills up or not, since it's against an "async" call, which shouldn't halt the caller.
The main question is: may I expect a StackOverflow exception in some case, or am I guaranteed to have it not?
What if I also remove the delay?
As for me, the pattern should be safe (almost) all the times, but honestly I wouldn't explain how to guarantee it (at least without analyzing the IL code behind). Just a doubt when the loop is very tight so that the async-call gets "slower" than the whole function itself.
No, you'll end up with a lot of continuations scheduled on a lot of tasks, rather than a deep stack... at least in the initial call.
Note that this is only because you're using
await Task.Delay(1000);
... which can reasonably be expected never to return a task which has already completed. If you had something like:
var value = await cache.GetAsync(key);
where it might easily return a completed task, then you really could end up with a very deep stack.
All you need to remember is that the call is synchronous until it hits the first await
with a non-completed awaitable.
What if I also remove the delay?
Then you'd effectively have a synchronous method which calls itself recursively, and unless the flag was set really soon, you'd end up with a stack overflow.
Note that even though you're not getting a stack overflow in the current code, you're still scheduling one new task per second until the flag is set, which isn't ideal - it's far from awful, as the tasks will die off quickly anyway, but I'd still use a loop if possible.
One potential issue is that I believe the "logical" stack is still captured... and that will get bigger and bigger as you go. It's possible that that's only the case when debugging though - I'm not an expert on it.
See more on this question at Stackoverflow