1

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.

svick
  • 236,525
  • 50
  • 385
  • 514
Mario Vernari
  • 6,649
  • 1
  • 32
  • 44

1 Answers1

4

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • That's making sense to me. The real case is much slower: it involves an HTTP roundtrip, plus a delay of at least one second. BTW, the case was pretty curious to ask something more about it! – Mario Vernari Jan 30 '14 at 16:10
  • @MarioVernari: I've just realized that you're not actually awaiting `DoIt`, so my concern around a lot of tasks later evaporates. Indeed, you won't even have many tasks alive at a time - just one or two, with a new one starting and an old one ending each second. – Jon Skeet Jan 30 '14 at 16:14
  • 2
    @MarioVernari: In your real case can you not just use a loop? It feels cleaner to me. – Jon Skeet Jan 30 '14 at 16:16
  • well, at this point I also think a normal loop (with cancellation) would be safer. The case is a WPF page which has to poll data from the server, and display the result: my deal is avoiding any possible leak / zombie task when the page *should* be garbaged. – Mario Vernari Jan 30 '14 at 16:19