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?

svick
  • 236,525
  • 50
  • 385
  • 514
Roger Johansson
  • 22,764
  • 18
  • 97
  • 193
  • 1
    So when does `Handle(socket)` ever run? – leppie May 30 '12 at 10:37
  • 2
    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); } }`? –  May 30 '12 at 10:39
  • @leppie After `AwaitSocket();` returns. And yes, it does return. –  May 30 '12 at 10:40
  • Also wont this cause LIFO order of processing? – leppie May 30 '12 at 10:41
  • 1
    @hvd: This stuff just hurts my brain! ;p – leppie May 30 '12 at 10:41
  • +1 Looking at it further, this doesn't seem as straightforward as I first though. Interesting. – Adam Houldsworth May 30 '12 at 10:41
  • No LIFO, the first accepted socket will be handled as soon as the await for the next socket starts. – Roger Johansson May 30 '12 at 10:42
  • So Adams first comment is incorrect, the code works, I just want to know if this is considered OK and safe. – Roger Johansson May 30 '12 at 10:43
  • 1
    @RogerAlsing Are you sure about that? The `await` may complete synchronously, and in that case, the order gets messed up. –  May 30 '12 at 10:43
  • @RogerAlsing Yeah after seeing it more clearly my comment was definitely incorrect, deleted. – Adam Houldsworth May 30 '12 at 10:43
  • @RogerAlsing: Interesting :) Still looks like it will blow the stack after some time (but I could be seriously wrong). – leppie May 30 '12 at 10:43
  • @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. – Adam Houldsworth May 30 '12 at 10:45
  • @RogerAlsing What happens when there is nothing coming from the socket thus the `await` doesn't return very promptly? – Adam Houldsworth May 30 '12 at 10:46
  • @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? – Roger Johansson May 30 '12 at 10:48
  • @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? – Adam Houldsworth May 30 '12 at 10:49
  • @AdamHouldsworth await always _returns directly_ but continues the execution after the await _once the result_ arrives. those are two differnet things. – Roger Johansson May 30 '12 at 10:50
  • @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. – Adam Houldsworth May 30 '12 at 10:51
  • but the exact same thing will happen in the continuation. in the continuation the code will recurse, await, return and finish the continuation. – Roger Johansson May 30 '12 at 10:54
  • 1
    @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. –  May 30 '12 at 10:54
  • @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. – Damien_The_Unbeliever May 30 '12 at 10:55
  • @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 cleaner – Roger Johansson May 30 '12 at 10:58
  • @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. – Adam Houldsworth May 30 '12 at 11:00
  • 1
    @hvd, what this adds to your loop-based code is that more instances of `Handle()` can run concurrently. – svick May 30 '12 at 11:06
  • @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)). – leppie May 30 '12 at 11:06
  • @leppie 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. – Adam Houldsworth May 30 '12 at 11:07
  • @svick It doesn't. If you don't explicitly allow handlers to run on a different thread, they run on the same thread. –  May 30 '12 at 11:08
  • @svick 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. –  May 30 '12 at 11:09
  • @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. – svick May 30 '12 at 11:20
  • @svick Ah, good point, I did make an unwarranted assumption there. –  May 30 '12 at 11:21
  • 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. – Motomotes Mar 23 '15 at 02:56

1 Answers1

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...
}
Roger Johansson
  • 22,764
  • 18
  • 97
  • 193
  • What happens if `AcceptSocketAsync()` or `HandleAsync()` throws an exception? – svick May 30 '12 at 11:24
  • 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? – Roger Johansson May 30 '12 at 11:26
  • 1
    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` unless they are event handlers (and therefore *must* be `void`). Think of it this way: `async void` is *allowed*, not *recommended*. – Stephen Cleary May 30 '12 at 13:07
  • 1
    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. – Stephen Cleary May 30 '12 at 13:09
  • 1
    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](http://nitoprograms.blogspot.com/2009/05/using-socket-as-server-listening-socket.html) describing this behavior. – Stephen Cleary May 30 '12 at 13:14