Is async recursion safe in C# (async ctp/.net 4.5)?
Asked Answered
W

1

21

In C# with async ctp or the vs.net 2011 beta we can write recursive code like this:

public async void AwaitSocket()
{
    var socket = await this.AcceptSocketAsync(); //await socket and >>return<< to caller
    AwaitSocket(); //recurse, note that the stack will never be deeper than 1 step since await returns..
    Handle(socket); // this will get called since "await" returns
}

In this specific sample, the code async waits for a tcp socket and once it has been accepted, it will recurse and async wait for another one.

This seems to work fine, since the await section will make the code return to the caller and thus, not cause a stack overflow.

So two questions here:

  1. if we ignore the fact we are dealing with sockets in this sample. Is it OK to do stack free recursion this way? or are there drawbacks Im missing?

  2. from an IO perspective, would the above code be enough to handle all incoming requests? I mean by just waiting for one, and once it is accepted start waiting for another one. Will some requests fail because of this somehow?

Wyn answered 30/5, 2012 at 10:30 Comment(30)
So when does Handle(socket) ever run?Ronel
I don't see anything wrong with it per se, but what does it add to the IMO more straightforward public async void AwaitSocket() { while (true) { var socket = await this.AcceptSocketAsync(); Handle(socket); } }?Pepperandsalt
@Ronel After AwaitSocket(); returns. And yes, it does return.Pepperandsalt
Also wont this cause LIFO order of processing?Ronel
@hvd: This stuff just hurts my brain! ;pRonel
+1 Looking at it further, this doesn't seem as straightforward as I first though. Interesting.Autobiography
No LIFO, the first accepted socket will be handled as soon as the await for the next socket starts.Wyn
So Adams first comment is incorrect, the code works, I just want to know if this is considered OK and safe.Wyn
@RogerAlsing Are you sure about that? The await may complete synchronously, and in that case, the order gets messed up.Pepperandsalt
@RogerAlsing Yeah after seeing it more clearly my comment was definitely incorrect, deleted.Autobiography
@RogerAlsing: Interesting :) Still looks like it will blow the stack after some time (but I could be seriously wrong).Ronel
@RogerAlsing It would blow the stack if lots of things are being awaited, enough calls to fill the memory. If stuff is being processed faster than the number of calls, then it should be fine. As this is all on the same socket, I'd guess this is fine, but in theory the stack is still open for the same abuse as before - just your use case is stopping it.Autobiography
@RogerAlsing What happens when there is nothing coming from the socket thus the await doesn't return very promptly?Autobiography
@AdamHouldsworth so if the usecase wasn't IO, but rather faster executing tasks, this might be a problem? also, since tasks are queued on the thread pool rather than the stack, wouldn't the exact same problem occur with the while loop?Wyn
@RogerAlsing The problem isn't to do with the tasks, it is to do with the method frames pushed onto the stack for a method call, in your case, the continuation from the await will contain a call back into the same method. To my eyes it doesn't look like it should return. Can you post a sample application so we can all have a play?Autobiography
@AdamHouldsworth await always returns directly but continues the execution after the await once the result arrives. those are two differnet things.Wyn
@RogerAlsing OK, but that still means the continuation will have the subsequent call into the method again and never get to touch Handle from what I can see. Unless I'm not seeing what code is run when it immediately returns as opposed to the code run when the await resolves.Autobiography
but the exact same thing will happen in the continuation. in the continuation the code will recurse, await, return and finish the continuation.Wyn
@RogerAlsing No, await doesn't always return directly, as I mentioned in an earlier comment. It may return directly, or if the result is already available without waiting, it may process the result directly.Pepperandsalt
@RogerAlsing - "await always returns directly" - not if the thing being awaited managed to complete synchronously - probably not an issue here, but it's a dangerous assumption that the first instance of await in a method necessarily implies a return to the caller at that point.Andriaandriana
@hvd OK, yes I see what you mean, if there were a socket already available for accept in the recursion, it would recurse deeper. hmmm so either go for the while loop or add an "await Task.Yield()" at the top to ensure return.. guess the while loop is cleanerWyn
@RogerAlsing It also expresses your intent more explicitly, a while condition loop tells people to expect it to keep running while a particular condition isn't met. Recursion for while semantics, though it may work, will be difficult to read at a glance.Autobiography
@hvd, what this adds to your loop-based code is that more instances of Handle() can run concurrently.Anastasio
@AdamHouldsworth: Problem with the while loop is a different semantic. With while, there could be a possible 'pause' between accepting consecutive clients (due to Handle(socket)).Ronel
@Ronel Ah yes very true, this async stuff hurts my brain as well apparently :-( The recursive code could work if it had logic to stop nesting after a certain number of calls.Autobiography
@Anastasio It doesn't. If you don't explicitly allow handlers to run on a different thread, they run on the same thread.Pepperandsalt
@Anastasio But that's possible with a loop too, by calling a HandleAsync(socket) which schedules a call to Handle(socket) that doesn't need to run on the same thread.Pepperandsalt
@hvd That depends on the context. You're right if this code is run on a UI thread, but not otherwise, because then the code will run on a ThreadPool thread, and there is nothing forcing only one thread at a time.Anastasio
@Anastasio Ah, good point, I did make an unwarranted assumption there.Pepperandsalt
I don't think recursion works with a stack and regular function call when calling an async, I think calling an async method causes a new thread each time.Undone
W
2

From the discussion above, I guess something like this will be the best approach. Please give feedback

public async void StartAcceptingSockets()
{
    await Task.Yield(); 
    // return to caller so caller can start up other processes/agents
    // TaskEx.Yield in async ctp , Task.Yield in .net 4.5 beta

    while(true)
    {
        var socket = await this.AcceptSocketAsync();
        HandleAsync(socket); 
        //make handle call await Task.Yield to ensure the next socket is accepted as fast 
        //as possible and dont wait for the first socket to be completely handled
    } 
}

private async void HandleAsync(Socket socket)
{
      await Task.Yield(); // return to caller

      ... consume the socket here...
}
Wyn answered 30/5, 2012 at 11:8 Comment(5)
What happens if AcceptSocketAsync() or HandleAsync() throws an exception?Anastasio
Yes what does exaclt happen if HandleAsync throws after the await yield part? if the continuation is handled in the threadpool and the code throws, this will terminate the thread pool thread, right?Wyn
If an exception is thrown from async void, it is passed directly to the "context". If the continuation is running in a thread pool context, this will cause an exception to be raised directly on a thread pool thread, crashing the process. Normally, you should have all async methods return Task/Task<TResult> unless they are event handlers (and therefore must be void). Think of it this way: async void is allowed, not recommended.Arne
I also don't see the need for Yield. The first call to AcceptSocketAsync will almost definitely cause a yield. Similarly for HandleAsync: if there's work to be done, you may as well do it; when you have to wait for something, then just have await implicitly yield for you.Arne
P.S. TcpListener.Start can take a backlog parameter. By default, this is set to the maximum value. backlog is the number of connections the OS will accept on your behalf, so you don't have to worry about accepting them "quickly". I have a blog post describing this behavior.Arne

© 2022 - 2024 — McMap. All rights reserved.