【问题标题】:Avoiding Deadlock with HttpClient使用 HttpClient 避免死锁
【发布时间】:2015-08-25 04:34:04
【问题描述】:

使用HttpClient 并避免死锁的最佳方法是什么?我正在使用下面的代码,完全从同步方法调用,但我担心它可能会导致死锁。

我已经阅读了诸如 .ConfigureAwait(false).GetAwaiter().GetResult() 之类的函数,但我正在寻找有关最佳实践方法的意见。

代码不完全准确,但足够接近。

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;
       }
    }
}

【问题讨论】:

  • 为什么你认为这段代码会导致死锁
  • 我在 HttpClient 上设置了超时,但我仍然偶尔会在高负载下看到问题(请求持续很长时间)。
  • 能否添加调用代码
  • @3dd 因为确实如此。 Task.Result 与 WinForms 或 WPF 中的 SynchronizationContext 一起使用时,几乎可以保证死锁,因为 Task.Result 将阻塞直到 Task.IsComplete,但是,Task 正在等待 SynchronizationContext 运行回调,导致死锁。
  • 感谢@Aron 说得通

标签: c#


【解决方案1】:

您似乎正在尝试同步运行异步代码。

使用 WinForms 和 WPF 您无法安全地执行此操作!

你唯一能做的就是一直使用异步。这是 .net 异步的一个已知问题。您可以使用public async void XXX() 方法。但是你不知道他们什么时候完成。你应该只在与事件处理程序结合使用时使用async void

您遇到死锁的原因是默认的TaskFactory 会尝试将中断回调编组回SynchronizationContext,这可能是您的UI 线程。

即使您使用Task.ConfigureAwait(false),也不能保证在调用堆栈的更下方您没有需要 UI 线程的回调。

只要阻塞SynchronizationContext线程,死锁的可能性就很大。

还值得注意的是,异步代码有时似乎可以工作。这是因为,允许返回 Task 的异步方法同步完成(例如 Task.Return<T>(T result))。具有缓存的方法(如 HttpRequests)通常会发生这种情况。

编辑:@SriramSakthivel 建议您可以通过将异步方法包装在Task.Run 中来同步运行它。这是因为 Task.Run 将在没有父 SynchronizationContext 的情况下运行代码。

Task.Run(RunRequest).Result;

我个人不推荐这样做,因为它依赖于 Task.RunTaskFactory 的具体实现才能工作。新版本的 .net 完全有可能(但不太可能)会破坏这段代码。

【讨论】:

  • 您是否建议使用 100% 同步,比如 WebClient?
  • @ChrisMorgan 取决于您的规模。 100% 同步不会像 100% 异步那样扩展。但唯一的一点是,同步代码永远不会调用异步代码。但是异步代码可以调用同步代码。
  • 您可以使用Task.Run 包装顶级异步方法,这将删除SynchronizationContext,因此不会出现死锁。
  • @SriramSakthivel 这是真的。我知道没有任何情况下这可能不起作用,但不能保证它适用于所有情况(尤其是在 .net 框架升级之后)。我可以推荐您的解决方案,但条件是它可能很脆弱并且依赖于特定的实现。
  • @Aron 这对 Winforms 和 WPF 来说 100% 有意义,但 OP 表示这是一个网站,那么会有同样行为的原因吗?
【解决方案2】:

我认为您的意思是避免阻塞。死锁是指两个或多个线程都无限期等待对方完成的情况。

为避免在您的示例代码中阻塞,而不是同步等待结果,您只需 await 非阻塞 API 调用:

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;
    }
}

【讨论】:

  • 感谢您的回答,但是所有代码都在同步方法中,所以 await 关键字不可用
  • @ChrisMorgan 你不能把方法签名改成async吗?
  • @ChrisMorgan 如果您可以更改签名,重新设计正在移动的数据将是微不足道的,例如废弃out 参数并返回Tuple
  • @SaebAmini 我不同意使用Tuple,我认为实际的课程更有意义。
  • @Aron 你可以,并且应该XML comments中记录方法返回的内容。
【解决方案3】:

在我的情况下,当我使用await client.GetAsync() 时,我没有收到任何响应,当我尝试观察问题时,调试器也终止于上述 GetAsync() 行。我只能使用client.GetAsync().Result 来正确获得响应,但我看到很多人说它可能会导致像post 这样的死锁,而且它也是同步的。我在 UI 按钮点击事件中也特别使用了这个,不推荐

最后,我通过添加以下三行来让它工作,我之前没有这三行(也许这是问题所在):

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);
  ...
}

注意:

  1. ServiceBaseAddress 必须以斜杠 (/) 结尾,并且相对 URL 必须以不带斜杠 (/) 开头
  2. 我的Constants.ServiceBaseAddress 的示例是https://domainname.org/

现在运行良好,具有异步签名且无需担心死锁。希望这对某人有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-17
    • 1970-01-01
    • 1970-01-01
    • 2018-09-11
    • 2020-10-25
    • 2012-10-30
    • 1970-01-01
    相关资源
    最近更新 更多