为什么使用async await时HTTP请求永远不会返回?

4

我正在尝试在运行于IIS 8.5上的ASP.NET应用程序中执行对另一个服务器的HTTP调用。

为了开始,我从Microsoft的一篇文章Call a Web API From a .NET Client (C#)中获取了一些提示。 我可以很容易地看到他们如何进行HTTP调用的模式; 这里只展示一个缩短的示例:

    static async Task<Product> GetProductAsync(string path)
    {
        HttpResponseMessage response = await client.GetAsync(path);
        if (response.IsSuccessStatusCode)
        {
            // retrieve response payload
            ... = await response.Content.ReadAsAsync<...>();
        }
        // do something with data
    }

我认为这很容易,所以我很快为我的应用程序编写了一个类似的方法(请注意,ReadAsAsync 扩展方法似乎需要一个额外的库 (链接),因此我选择了其中一个内置的、更抽象但可能类似的方法):

    private async Task<MyInfo> RetrieveMyInfoAsync(String url)
    {
        var response = await HttpClient.GetAsync(url);

        response.EnsureSuccessStatusCode();
        var responseBody = await response.Content.ReadAsStringAsync();

        return JsonConvert.DeserializeObject<MyInfo>(responseBody);
    }

不幸的是,调用这个方法会导致我的应用程序挂起。在调试时,发现对GetAsyncawait调用从未返回。

搜索了一下,我偶然发现一个类似的问题,在其评论部分中,我找到了Mr. B提出的非常有趣的建议

删除所有异步内容,并确保它可行。

所以我试了一下:

    private Task<MyInfo> RetrieveMyInfoAsync(String url)
    {
        return HttpClient.GetAsync(url).ContinueWith(response =>
        {
            response.Result.EnsureSuccessStatusCode();
            return response.Result.Content.ReadAsStringAsync();
        }).ContinueWith(str => JsonConvert.DeserializeObject<MyInfo>(str.Result.Result));
    }

有点出乎我的意料,这个方法有效。 GetAsync 在不到一秒的时间内从另一个服务器返回了预期的响应。

现在,同时使用AngularJS时,像response.Result.Contentstr.Result.Result这样的东西让我有点失望。在AngularJS中,我希望上面的调用只是简单的:

$http.get(url).then(function (response) {
    return response.data;
});

即使我们忽略在JavaScript中自动进行的JSON反序列化,AngularJS代码仍然更容易,例如response没有被包装成promise或类似的东西,也不会在继续函数中返回另一个promise时出现像Task<Task<...>>这样的结构。
因此,如果后者正常工作,我不太喜欢使用这个ContinuesWith语法而不是更易读的async-await模式。
在我的C# HTTP调用的async-await变体中,我做错了什么?

尝试使用await HttpClient.GetAsync(url).ConfigureAwait(false),并告诉我是否有帮助。 - V0ldek
你是否在任何地方使用了 Wait()Result 或者 GetResult() - FCin
@V0ldek:确实,那很有帮助。那里发生了什么? - O. R. Mapper
然后 GetAwaiter().GetResult() 导致了死锁。请查看我的答案以获得更好的解释。 - V0ldek
非常好的对话,我遇到了同样的问题,通过你们的讨论我得到了线索并解决了我的问题。谢谢 @V0ldek 和 O.R.Mapper。 - undefined
显示剩余2条评论
1个回答

8
根据您使用ConfigureAwait(false)有所帮助的事实,建议您阅读Stephen Cleary的博客:http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html。他是一位非常专业的异步编程专家(撰写了《C# Cookbook》),他能更好地解释问题。您可能在某个地方阻塞了ASP.NET线程,可能没有完全使用await而是使用WaitResultGetResult()。您可以使用该博客自行诊断问题。 ConfigureAwait(false)的作用是不捕获当前上下文,因此HTTP请求会在ASP.NET上下文之外(正确地)执行,从而避免死锁。
根据您的评论,GetAwaiter().GetResult()导致了问题。如果将其更改为await并将调用方法更改为async,您可能会解决所有问题。
自从C# 7.0和async Task Main()方法支持以来,在应用程序代码中阻塞而不使用await的原因已经不存在了。

区别在于当您设置ConfigureAwait(false)时,请求线程将进入以恢复该方法。 - Schadensbegrenzer
如果你将其改为await并将调用方法更改为async,你可能会解决所有问题。我之前不知道可以将ASP.NET Web API控制器方法声明为async,但显然这是可行的。现在我遇到了“异步模块或处理程序在仍有异步操作挂起时完成”的异常,但我认为这与此特定问题无关,所以我将其标记为已接受。 - O. R. Mapper
他没有为广告付款,但如果你对async/await或其他与C#并发相关的东西感兴趣,那么我提到的书是一本不错的读物。 - V0ldek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接