0

I have to apply for 2 approches:

  1. handle each HTTP request with a different thread/task.
  2. finish the requests gracefully when the cancellation token invoked.

Now, I have a problem that when the cancellation token invoked and a request doesn't arrive - I stack in the "server.getContext" blocking area. Any idea how can I solve it?

public void Listen()
{
  CancellationTokenSource source = new CancellationTokenSource();
  CancellationToken token = source.Token;
  server.Start();
  Console.WriteLine($"Waiting for connections on {url}");
  HandleShutdownWhenKeyPressed(source);
  HandleIncomingHTTPRequests(token);
  server.Close();
}

void HandleIncomingHTTPRequests(CancellationToken token)
{
  while (!token.IsCancellationRequested)
  {
    HttpListenerContext ctx = server.GetContext();
    // I stack here until the request has arrived even if the cancellation token has invoked.
    Task.Run(() =>
    {
      HttpListenerRequest req = ctx.Request;
      HttpListenerResponse res = ctx.Response;
      // SOME STUFF ....
    },token)
  }
 }

Thanks in advance.

Eliran Suisa
  • 49
  • 1
  • 8
  • "handle each HTTP request with a different thread/task." - this is actually two separate approaches: using `Task` implies **not** using different Threads because you'll be using async IO and the thread pool. – Dai Jan 03 '21 at 09:29
  • Your `HandleIncomingHTTPRequests` method should be marked as `async Task` **not** `void` - and you don't need to use `Task.Run`. – Dai Jan 03 '21 at 09:30
  • hi @Dai, why not? if I want to perform the request with different tasks (like different threads from the thread pool ) - how can I achieve it otherwise? – Eliran Suisa Jan 03 '21 at 09:31
  • Use `HttpListener.GetContextAsync` - it handles asynchronous IO for you. – Dai Jan 03 '21 at 09:32
  • @Dai - as far as I know - Differnet threads from threadpool != async IO action. am I write ? – Eliran Suisa Jan 03 '21 at 09:36
  • No, that is incorrect. Async IO does not require the thread-pool: you can have async IO in a single-threaded process (provided the entrypoint thread eventually yields to the Task Scheduler (the entrypoint thread is also-known-as the "main thread" or the "UI thread" in desktop GUI programs). Granted, *in practice* in .NET most async IO will resume in a thread-pool thread, but this is not required and must not be assumed. – Dai Jan 03 '21 at 09:38
  • When closing the HttpListener, it should unstuck the thread waiting on `server.GetContext()`. This will throw an exception but this is expected, you can just catch it and exit: `catch (Exception) when (!server.IsListening) { return; }` That said, as mentioned by Dai, you can also use `GetContextAsync` to avoid blocking on GetContext – Kevin Gosse Jan 03 '21 at 09:43
  • hi @KevinGosse, this is accepting the "finish the requests gracefully" and let them all finish ? – Eliran Suisa Jan 03 '21 at 09:45
  • @EliranSuisa I'm not sure it's technically possible to "let all requests finish". If you receive another requests while you're processing the last ones, what do you do? If you don't process it then you're not processing all the requests gracefully. If you do, then you might receive another one, and another one, and basically never finish – Kevin Gosse Jan 03 '21 at 09:47
  • Depending on your use case, you could also trying using [Grapevine](https://github.com/scottoffen/grapevine) instead of rolling your own implementation. Or just check out the code there and see how I did it. – Scott Offen Aug 26 '21 at 14:54

1 Answers1

0

You don't need Task.Run, and the beauty of async IO is that you don't need to handle Thread,ThreadPool or Task.Run yourself: IO completion is already handled for you by the actual implementation (in this case, HttpListener, but in other cases it could be FileStream, Socket, etc).

All you need is this:

public static async Task RunHttpListenerAsync( HttpListener listener, CancellationToken ct )
{
    listener.Start();

    while( !ct.IsCancellationRequested )
    {
        HttpListenerContext ctx = await listener.GetContextAsync().ConfigureAwait(false);
        
        try
        {
            await ProcessRequestAsync( ctx ).ConfigureAwait(false);
        }
        catch( Exception ex )
        {
            // TODO: Log `ex`.
        }
    }
}

private static async Task ProcessRequestAsync( HttpListenerContext ctx )
{
    HttpListenerRequest  req = ctx.Request;
    HttpListenerResponse res = ctx.Response;

    // do stuff
}

Provided your program runs without a synchronization context that requires resumption on a single thread (i.e. you aren't running inside WinForms or WPF) - or - you use .ConfigureAwait(false) everywhere (like you should) then every time your program yields to the scheduler it will resume on whatever background thread in the thread-pool it wants, including any available threads that would resume the current Task concurrently.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • 2
    This is not correct. OP's code is capable of handling multiple requests in parallel. Yours will always process only one at a time. While using `await listener.GetContextAsync()` is good because it prevents having one thread waiting for it, you still need `Task.Run` to process the requests – Kevin Gosse Jan 03 '21 at 09:43
  • 1
    @KevinGosse "Yours will always process only one at a time" - that's an oversimplification. Yes: the code I posted will process new incoming requests in a _serial_ fashion, however once a request has entered `ProcessRequestAsync` those tasks may resume _concurrently_ - and serialization of `GetContextAsync` is rarely a bottleneck in web-applications. – Dai Jan 03 '21 at 09:47
  • 1
    well yes. You changed a code that **will** process requests concurrently, to one that **may** process them concurrently assuming `ProcessRequestAsync` has any async part, which OP never mentioned – Kevin Gosse Jan 03 '21 at 09:48
  • 1
    @KevinGosse "You changed a code that **will** process requests concurrently" - that would be suboptimal if the program was running inside a machine with only a single hardware thread. Similarly the "may" in my answer is in reference to the fact the task scheduler in the CLR can be trusted to run with the optimum number of threads given the hardware (or VM) constraints. – Dai Jan 03 '21 at 09:50
  • 1
    Just so we're clear, your code is mostly correct, but you should just have written `_ = Task.Run(() => ProcessRequests(ctx);` instead of `await ProcessRequestAsync( ctx )...` – Kevin Gosse Jan 03 '21 at 09:50
  • 1
    `Similarly the "may" in my answer is in reference to the fact the task scheduler in the CLR can be trusted to run with the optimum number of threads given the hardware (or VM) constraints.` there again, this will only happen if `ProcessRequestAsync` actually yield on some asynchronous workload, which we have no guarantee over. If `ProcessRequestAsync` runs synchronously, then any other requests received in the timespan will be waiting in the queue, even if the server has capabilities to process them – Kevin Gosse Jan 03 '21 at 09:52
  • 1
    With respect, the secondary point of my answer is that `Task.Run` is largely unnecessary in this case. Do you have any benchmarks or papers that support your position that **it's always preferable** to use `Task.Run`? My position is that `Task.Run` should be avoided unless profiling (and available hardware) says otherwise: for example: https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html and especially: https://stackoverflow.com/questions/18013523/when-correctly-use-task-run-and-when-just-async-await – Dai Jan 03 '21 at 09:52
  • 1
    That's really not a matter of benchmark. If all `ProcessRequestAsync` is doing is `await GetDataFromCache(); ProcessStuffFor5Seconds();`, and if `GetDataFromCache()` returns synchronously most of the time (because the cache is populated), then your server will only be processing one request per 5 seconds. Asynchronicity is nice to have for a server, but parallelism is most of the time required – Kevin Gosse Jan 03 '21 at 09:54
  • 1
    @KevinGosse It's 2am here so I'll just agree to disagree :) – Dai Jan 03 '21 at 09:55
  • 1
    Fair, there's all the information required in those comments for OP to make an educated decision – Kevin Gosse Jan 03 '21 at 09:55
  • The "listener.GetContextAsync()" is finishing immideatily when starting the program, any idea why ? – Eliran Suisa Jan 03 '21 at 10:35