【问题标题】:HttpClient.GetAsync(...) never returns when using await/asyncHttpClient.GetAsync(...) 在使用 await/async 时永远不会返回
【发布时间】:2012-05-07 18:55:31
【问题描述】:

编辑: This question 看起来可能是同样的问题,但没有任何回应...

编辑:在测试用例 5 中,任务似乎卡在WaitingForActivation 状态。

我在 .NET 4.5 中使用 System.Net.Http.HttpClient 时遇到了一些奇怪的行为——“等待”(例如)httpClient.GetAsync(...) 的调用结果永远不会返回。

这仅在使用新的 async/await 语言功能和 Tasks API 的特定情况下发生 - 仅使用延续时代码似乎总是有效。

这里有一些重现问题的代码 - 将其放入 Visual Studio 11 中的新“MVC 4 WebApi 项目”中,以公开以下 GET 端点:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

这里的每个端点都返回相同的数据(来自 stackoverflow.com 的响应标头),但永远不会完成的 /api/test5 除外。

我是否在 HttpClient 类中遇到了错误,或者我是否以某种方式滥用了 API?

要重现的代码:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}

【问题讨论】:

  • 这似乎不是同一个问题,但只是为了确保您了解它,在同步完成的 beta WRT 异步方法中有一个 MVC4 错误 - 请参阅 stackoverflow.com/questions/9627329/…
  • 谢谢 - 我会注意的。在这种情况下,我认为该方法应该始终是异步的,因为调用了HttpClient.GetAsync(...)?

标签: c# .net asynchronous async-await dotnet-httpclient


【解决方案1】:

您在滥用 API。

情况如下:在 ASP.NET 中,一次只有一个线程可以处理一个请求。如有必要,您可以进行一些并行处理(从线程池中借用额外的线程),但只有一个线程具有请求上下文(其他线程没有请求上下文)。

这是managed by the ASP.NET SynchronizationContext

默认情况下,当您awaitTask 时,该方法会在捕获的SynchronizationContext(或捕获的TaskScheduler,如果没有SynchronizationContext)上恢复。通常,这正是您想要的:异步控制器操作将await 某些东西,当它恢复时,它会根据请求上下文恢复。

所以,这就是test5 失败的原因:

  • Test5Controller.Get 执行 AsyncAwait_GetSomeDataAsync(在 ASP.NET 请求上下文中)。
  • AsyncAwait_GetSomeDataAsync 执行 HttpClient.GetAsync(在 ASP.NET 请求上下文中)。
  • HTTP 请求发出,HttpClient.GetAsync 返回未完成的Task
  • AsyncAwait_GetSomeDataAsync 等待Task;因为它不完整,AsyncAwait_GetSomeDataAsync 返回一个未完成的Task
  • Test5Controller.Get 阻塞当前线程,直到 Task 完成。
  • HTTP响应进来,HttpClient.GetAsync返回的Task完成。
  • AsyncAwait_GetSomeDataAsync 尝试在 ASP.NET 请求上下文中恢复。但是,该上下文中已经有一个线程:该线程在 Test5Controller.Get 中阻塞。
  • 死锁。

这就是其他方法起作用的原因:

  • test1test2test3):Continuations_GetSomeDataAsync 将继续调度到线程池, ASP.NET 请求上下文。这允许Continuations_GetSomeDataAsync 返回的Task 完成,而无需重新进入请求上下文。
  • test4test6):由于Task等待,因此不会阻塞 ASP.NET 请求线程。这允许AsyncAwait_GetSomeDataAsync 在准备好继续时使用 ASP.NET 请求上下文。

以下是最佳做法:

  1. 在您的“库”async 方法中,尽可能使用ConfigureAwait(false)。在您的情况下,这会将 AsyncAwait_GetSomeDataAsync 更改为 var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要屏蔽Tasks;一直是async。换言之,使用await 代替GetResultTask.ResultTask.Wait 也应替换为await)。

这样,您可以获得两个好处:延续(AsyncAwait_GetSomeDataAsync 方法的其余部分)在不必进入 ASP.NET 请求上下文的基本线程池线程上运行;控制器本身是async(不会阻塞请求线程)。

更多信息:

2012 年 7 月 13 日更新: 包含此答案 into a blog post

【讨论】:

  • 是否有一些 ASP.NET SynchroniztaionContext 的文档说明在上下文中只能有一个线程来处理某些请求?如果没有,我认为应该有。
  • AFAIK 的任何地方都没有记录。
  • 谢谢 - 真棒响应。 (显然)功能相同的代码之间的行为差​​异令人沮丧,但您的解释是有道理的。如果框架能够检测到此类死锁并在某处引发异常,那将很有用。
  • 是否存在不建议在 asp.net 上下文中使用 .ConfigureAwait(false) 的情况?在我看来,它应该始终被使用,并且它只在 UI 上下文中不应该使用,因为您需要同步到 UI。还是我没有抓住重点?
  • ASP.NET SynchronizationContext 确实提供了一些重要的功能:它流动请求上下文。这包括从身份验证到 cookie 再到文化的各种内容。所以在 ASP.NET 中,不是同步回 UI,而是同步回请求上下文。这可能很快就会改变:新的 ApiController 确实有一个 HttpRequestMessage 上下文作为属性 - 所以 可能 不需要通过 SynchronizationContext 传递上下文 - 但我还不知道.
【解决方案2】:

编辑:通常尽量避免执行以下操作,除非作为避免死锁的最后努力。阅读 Stephen Cleary 的第一条评论。

来自here 的快速修复。而不是写:

Task tsk = AsyncOperation();
tsk.Wait();

试试:

Task.Run(() => AsyncOperation()).Wait();

或者如果您需要结果:

var result = Task.Run(() => AsyncOperation()).Result;

来自源(编辑以匹配上面的例子):

现在将在 ThreadPool 上调用 AsyncOperation,其中 不会是 SynchronizationContext,内部使用的延续 的 AsyncOperation 不会被强制返回到调用线程。

对我来说,这似乎是一个可用的选项,因为我没有选择让它一直异步(我更喜欢)。

来源:

确保 FooAsync 方法中的 await 没有找到要执行的上下文 元帅回。最简单的方法是调用 来自 ThreadPool 的异步工作,例如通过包装 在 Task.Run 中调用,例如

int Sync() { 返回 Task.Run(() => Library.FooAsync()).Result; }

FooAsync 现在将在 ThreadPool 上调用,其中不会有 SynchronizationContext,以及 FooAsync 内部使用的延续 不会被强制返回调用 Sync() 的线程。

【讨论】:

  • 可能想重新阅读您的源链接;作者建议不要这样做。它有效吗?是的,但仅限于避免僵局。此解决方案否定了所有 ASP.NET 上async 代码的好处,实际上可能会导致大规模问题。顺便说一句,ConfigureAwait 在任何情况下都不会“破坏正确的异步行为”;这正是您应该在库代码中使用的内容。
  • 这是整个第一部分,以粗体标题Avoid Exposing Synchronous Wrappers for Asynchronous Implementations。整个帖子的其余部分都在解释几种不同的方法来做到这一点如果你绝对需要
  • 添加了我在源代码中找到的部分 - 我将留给未来的读者来决定。请注意,您通常应尽量避免这样做,并且仅将其作为最后的手段(即,使用您无法控制的异步代码时)。
  • 我喜欢这里的所有答案,并且一如既往....它们都基于上下文(双关语,哈哈)。我正在用同步版本包装 HttpClient 的 Async 调用,因此我无法更改该代码以将 ConfigureAwait 添加到该库中。因此,为了防止生产中的死锁,我将异步调用包装在 Task.Run 中。据我了解,这将在每个请求中使用 1 个额外线程并避免死锁。我假设要完全兼容,我需要使用 WebClient 的同步方法。需要做大量工作才能证明其合理性,所以我需要一个令人信服的理由,而不是坚持我目前的做法。
  • 我最终创建了一个扩展方法来将异步转换为同步。我在这里读到它与.Net框架相同的方式: public static TResult RunSync(this Func> func) { return _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); }
【解决方案3】:

由于您使用的是.Result.Waitawait,这最终会在您的代码中导致死锁

您可以在async 方法中使用ConfigureAwait(false)防止死锁

像这样:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

您可以尽可能使用ConfigureAwait(false) 来表示不要阻塞异步代码。

【讨论】:

    【解决方案4】:

    这两所学校并不是真的排斥。

    这是你必须使用的场景

       Task.Run(() => AsyncOperation()).Wait(); 
    

    或类似的东西

       AsyncContext.Run(AsyncOperation);
    

    我有一个 MVC 操作,它位于数据库事务属性下。这个想法(可能)是在出现问题时回滚操作中所做的所有事情。这不允许上下文切换,否则事务回滚或提交将自行失败。

    我需要的库是异步的,因为它预计会异步运行。

    唯一的选择。将其作为正常的同步调用运行。

    我只是对每个人自己说。

    【讨论】:

    • 所以你建议你的答案中的第一个选项?
    • > “我需要的库是异步的,因为它预计会异步运行。”这才是真正的问题。
    【解决方案5】:

    我将把它放在这里更多是为了完整性,而不是与 OP 直接相关。我花了将近一天的时间来调试一个HttpClient 请求,想知道为什么我从来没有收到回复。

    终于发现我忘记了awaitasync在调用栈下面调用了。

    感觉就像少了一个分号一样好。

    【讨论】:

      【解决方案6】:

      在我的情况下,“等待”从未完成,因为执行请求时出现异常,例如服务器没有响应等。用 try..catch 包围它以识别发生了什么,它也会优雅地完成你的“等待”。

      public async Task<Stuff> GetStuff(string id)
      {
          string path = $"/api/v2/stuff/{id}";
          try
          {
              HttpResponseMessage response = await client.GetAsync(path);
              if (response.StatusCode == HttpStatusCode.OK)
              {
                  string json = await response.Content.ReadAsStringAsync();
                  return JsonUtility.FromJson<Stuff>(json);
              }
              else
              {
                  Debug.LogError($"Could not retrieve stuff {id}");
              }
          }
          catch (Exception exception)
          {
              Debug.LogError($"Exception when retrieving stuff {exception}");
          }
          return null;
      }
      

      【讨论】:

      • 这是一个被忽视的答案。有时错误并不明显。例如,如果您在 OnIntialised 中初始化一个变量,如果渲染引擎尝试从该变量构建,则渲染引擎可能会抛出错误,因为线程仍在等待,那么整个事情就会崩溃,任何地方都没有错误。这解决了我数周以来一直在研究的问题。
      【解决方案7】:

      我使用了很多等待,所以我没有得到响应,我转换为同步调用它开始工作

                  using (var client = new HttpClient())
                  using (var request = new HttpRequestMessage())
                  {
                      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                      request.Method = HttpMethod.Get;
                      request.RequestUri = new Uri(URL);
                      var response = client.GetAsync(URL).Result;
                      response.EnsureSuccessStatusCode();
                      string responseBody = response.Content.ReadAsStringAsync().Result;
                     
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-05-08
        • 1970-01-01
        • 2018-05-16
        • 1970-01-01
        • 1970-01-01
        • 2020-09-21
        • 2013-11-23
        • 2017-05-25
        相关资源
        最近更新 更多