6

What is the best way to use HttpClient and avoid deadlock? I am using the code below, called entirely from synchronous methods, but I concerned it maybe causing a deadlock.

I've done some reading on functions like .ConfigureAwait(false), .GetAwaiter(), .GetResult() but I am looking for input on the best practice approach.

Not quite exact code, but close enough.

public static bool TryRequest(string url, out response)
{    
    HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");
    using (HttpClient client = new HttpClient())
    {
       HttpResponseMessage responseMessage = null;

       switch (verb)
       {
          case HttpVerb.Put:
             responseMessage = client.PutAsync(url, content).Result;
             break;
          case HttpVerb.Post:
             responseMessage = client.PostAsync(url, content).Result;
             break;
          case HttpVerb.Delete:
             responseMessage = client.DeleteAsync(url).Result;
             break;
          case HttpVerb.Get:
             responseMessage =  client.GetAsync(url).Result;
             break;
       }

       if (responseMessage.IsSuccessStatusCode)
       {
          responseContent = responseMessage.Content.ReadAsStringAsync().Result;
          statusCode = responseMessage.StatusCode;
       }
    }
}
Chris Morgan
  • 700
  • 6
  • 19
  • 1
    Why do you think this code will cause a dead-lock – 3dd Aug 25 '15 at 04:47
  • I have a timeout set on the HttpClient, yet I am still occasionally seeing issues (requests lasting a long time) under high load. – Chris Morgan Aug 25 '15 at 04:51
  • Can you add the calling code – 3dd Aug 25 '15 at 04:52
  • 2
    @3dd because it does. `Task.Result` when used with a `SynchronizationContext` from either WinForms or WPF is almost guarenteed to deadlock as `Task.Result` will block until the `Task.IsComplete`, however, the `Task`, is waiting for the `SynchronizationContext` to run the callback, resulting in a deadlock. – Aron Aug 25 '15 at 04:53
  • Thanks @Aron that makes sense – 3dd Aug 25 '15 at 04:56
  • @Aron Why the task should be waiting for synchronization context? Remember this is not an async method. – Sriram Sakthivel Aug 25 '15 at 05:04
  • @SriramSakthivel `client.GetAsync(url).Result;`? I kind of assumed that `client.GetAsync(url)` returned a `Task`. Within the body of the `.GetAsync` I would expect that there is an `await` on the `WebRequest.GetHttpResponseTaskAsync`. – Aron Aug 25 '15 at 05:05
  • @Aron Yes true it returns a `Task` but why should it deadlock when called synchronously? There is no asynchronous method (with async keyword) here. That's what will queue the continuation to synchronization context. Not the case here. Correct me if I'm wrong. – Sriram Sakthivel Aug 25 '15 at 05:07
  • @SriramSakthivel I expect that further down the call stack on the (presumably) `WebRequest.GetHttpResponseTaskAsync`. – Aron Aug 25 '15 at 05:07
  • @Aron Well, we don't know the implementation. So yes assume the worst (i.e it can deadlock). :) – Sriram Sakthivel Aug 25 '15 at 05:08
  • @SriramSakthivel granted, there was a certain amount of psychic debugging there. But in experience the deadlocking case is more common than the not case. – Aron Aug 25 '15 at 05:11

3 Answers3

9

It looks like you are trying to run asynchronous code synchronously.

With WinForms and WPF YOU CANNOT SAFELY DO THIS!

The only thing you can do is to use async all the way up. Its a known problem with .net async. You can use public async void XXX() methods. But then you don't know when they complete. You should ONLY ever use async void when coupled with an event handler.

The reason you are getting deadlocks is that the default TaskFactory will try to marshal interupt callbacks back to the SynchronizationContext, which is likely your UI thread.

Even if you use Task.ConfigureAwait(false) there is no guarantee that further down the callstack you don't have a callback which expects the UI thread.

As long as you block the SynchronizationContext thread, there is a very high possibility that you will deadlock.

It is also worth noting that it is possible that the asynchronous code seems to sometimes work. This is because, an async method that returns a Task, is allowed to synchronously complete (for example Task.Return<T>(T result)). This will often happen with methods that have a cache (like HttpRequests).

EDIT: @SriramSakthivel suggest that you can run an async method synchronously by wrapping it within Task.Run. This is because Task.Run will run the code without the parent SynchronizationContext.

Task.Run(RunRequest).Result;

I personally do not recommend this, as it relies on the specific implementation of Task.Run along with TaskFactory to work. It is entirely possible (but unlikely) that a new version of .net will break this piece of code.

Aron
  • 15,464
  • 3
  • 31
  • 64
  • Would you suggest going 100% synchronous, with say WebClient? – Chris Morgan Aug 25 '15 at 05:04
  • 1
    @ChrisMorgan depends on your scale. 100% synchronous will not scale like 100% async. But the only point is that, synchronous code may NEVER call async code. But async code CAN call synchronous code. – Aron Aug 25 '15 at 05:09
  • 3
    You can wrap the top level async method with `Task.Run` which will remove the `SynchronizationContext` so no deadlock. – Sriram Sakthivel Aug 25 '15 at 05:10
  • @SriramSakthivel that is true. I know of no cases where this might not work, but there is no guarantee that it will for all cases (especially after a .net framework upgrade). I can recommend your solution with the proviso that it is perhaps fragile and dependent on a specific implementation. – Aron Aug 25 '15 at 05:14
  • @Aron this makes 100% sense for Winforms and WPF, but OP stated that this is a website, would there be a reason for the same behavior then? – 3dd Aug 25 '15 at 05:21
  • @3dd yes. Although ASP only uses `ThreadPool` threads, each `HttpRequest` actually is associated with a `Thread`. We don't have the same level of strictness involved. But there is an `ExecutionContext` associated with the request. So the default `IAwaitable` will try to run on the original thread to preserve the `ExecutionContext`. – Aron Aug 25 '15 at 05:25
  • @Aron interesting and I assume you would get the deadlock if the original `Thread` is not available anymore or is busy doing something else. – 3dd Aug 25 '15 at 05:30
  • @3dd No. Its worse than that. If two requests share the same `Thread`. Then a deadlock on one request will cause the second request to also be Starved of the original `Thread`. – Aron Aug 25 '15 at 05:40
  • @Aron ah of course, thus with larger load on the server things might progressively get worse. Thanks for all the info Aron, greatly appreciated. – 3dd Aug 25 '15 at 05:48
  • @3dd It won't get "progressively" worse, as the deadlocked threads will not be called upon to handle "new" Requests. But yes, as load increases, the number of Requests that will be starved per deadlock will increase. – Aron Aug 25 '15 at 05:50
0

I think you mean avoid blocking. Deadlocks refer to situations where two or more threads are all indefinitely waiting for each other to finish.

To avoid blocking in your sample code, instead of synchronously waiting on Results, you just need to await the non-blocking API calls:

HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");
using (HttpClient client = new HttpClient())
{
    HttpResponseMessage responseMessage = null;

    switch (verb)
    {
        case HttpVerb.Put:
            responseMessage = await client.PutAsync(url, content);
            break;
        case HttpVerb.Post:
            responseMessage = await client.PostAsync(url, content);
            break;
        case HttpVerb.Delete:
            responseMessage = await client.DeleteAsync(url);
            break;
        case HttpVerb.Get:
            responseMessage = await client.GetAsync(url);
            break;
    }

    if (responseMessage.IsSuccessStatusCode)
    {
        responseContent = await responseMessage.Content.ReadAsStringAsync();
        statusCode = responseMessage.StatusCode;
    }
}
Saeb Amini
  • 23,054
  • 9
  • 78
  • 76
  • Thanks for the answer, but all code is inside synchronous methods, so await keyword is not available – Chris Morgan Aug 25 '15 at 04:47
  • @ChrisMorgan can't you change the method signature to be `async`? – Saeb Amini Aug 25 '15 at 04:47
  • @ChrisMorgan if you can change the signature, redesigning the data being moved around would be trivial, e.g. scrapping the `out` parameter and returning a `Tuple` instead. – Saeb Amini Aug 25 '15 at 04:53
  • @SaebAmini I disagree with using a `Tuple`, I think an actual class makes much more sense. – Aron Aug 25 '15 at 05:03
  • If I add await here, I'll need to edit all the way up the call stack to my Controller. That is not possible at this time. Right? – Chris Morgan Aug 25 '15 at 05:05
  • @ChrisMorgan why not? – Aron Aug 25 '15 at 05:10
  • @Aron actually, using a `Tuple` instead of `out` or `ref` parameters in async methods makes more sense _if_ using an `out` or `ref` parameter was sensible _in the first place_. If it was not, that's surely bad design. – Saeb Amini Aug 25 '15 at 05:13
  • 1
    @SaebAmini I disagree. `out` and `ref` will give you documentation in the form of the parameter name and possibly xml comments. `Tuple` will give you neither. A full blown class will again give you those two forms of inline documentation. – Aron Aug 25 '15 at 05:16
  • 1
    @Aron you can, and _should_ document what a method returns too, in [XML comments](https://msdn.microsoft.com/en-us/library/4dcfdeds.aspx). – Saeb Amini Aug 25 '15 at 05:27
0

In my case, when I used await client.GetAsync(), I didnot receive any response, when I tried to observe the problem, the debugger also terminated on the above GetAsync() line. I could only use client.GetAsync().Result to get the reponse correctly, but I saw many saying that it may lead to deadlock like in this post and it is synchronous too. I was particularly using this in UI button click event as well, which is not recommended.

Finally, I got it working by adding the below three lines, I didnot have those three lines before (maybe that was the problem):

using (var client = new HttpClient())
{    
  string relativeURL = "api/blah";
  //These three lines - BaseAddress, DefaultRequestHeaders and DefaultRequestHeaders
  client.BaseAddress = new Uri(Constants.ServiceBaseAddress);
  client.DefaultRequestHeaders.Clear();
  client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
  HttpResponseMessage response = await client.GetAsync(relativeURL);
  ...
}

NOTE:

  1. The ServiceBaseAddress must end with slash (/) and relative URL must begin without slash (/)
  2. Sample of my Constants.ServiceBaseAddress is https://domainname.org/

This is working fine now, with async signature and no deadlock worries. Hope this helps somebody.

Mac
  • 373
  • 5
  • 20