使用await/async时,HttpClient.GetAsync(...)永远不会返回

Ben*_*Fox 297 .net c# asynchronous async-await dotnet-httpclient

编辑: 这个问题看起来可能是同一个问题,但没有回复......

编辑:在测试用例5中,任务似乎陷入WaitingForActivation状态.

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

只有在使用新的async/await语言功能和Tasks API时才会出现这种情况 - 在仅使用continuation时,代码似乎始终有效.

下面是一些重现问题的代码 - 将其放入Visual Studio 11中新的"MVC 4 WebApi项目"中,以显示以下GET端点:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6
Run Code Online (Sandbox Code Playgroud)

这里的每个端点都返回相同的数据(来自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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*ary 450

您滥用API.

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

这是由ASP.NET管理的SynchronizationContext.

默认情况下,当您执行awaita时Task,该方法将在捕获的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响应进来,Task返回的HttpClient.GetAsync是完成的.
  • AsyncAwait_GetSomeDataAsync尝试在ASP.NET请求上下文中恢复.但是,在该上下文中已经存在一个线程:阻塞的线程Test5Controller.Get.
  • 僵局.

这就是其他人工作的原因:

  • (test1,test2test3):在ASP.NET请求上下文之外Continuations_GetSomeDataAsync调度线程池的延续.这允许返回者完成而无需重新输入请求上下文.TaskContinuations_GetSomeDataAsync
  • (test4test6):自Task期待已久的,ASP.NET请求线程不被阻塞.这允许AsyncAwait_GetSomeDataAsync在准备好继续时使用ASP.NET请求上下文.

这是最好的做法:

  1. 在"库" async方法中,ConfigureAwait(false)尽可能使用.在你的情况下,这将改变AsyncAwait_GetSomeDataAsyncvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要阻挡Task; 它async一直在下降.换句话说,使用await替代GetResult(Task.ResultTask.Wait也应更换await).

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

更多信息:

更新2012-07-13:将此答案纳入博客文章.

  • 谢谢 - **真棒**回复.(显然)功能相同的代码之间的行为差​​异是令人沮丧的,但你的解释是有道理的.如果框架能够检测到这种死锁并在某处引发异常,那将是有用的. (9认同)
  • AFAIK的任何地方都没有记录. (7认同)
  • 是否存在不建议在asp.net上下文中使用.ConfigureAwait(false)的情况?在我看来它应该总是被使用,并且它只在UI上下文中它不应该被使用,因为你需要同步到UI.或者我错过了这一点? (3认同)
  • ASP.NET`SynchronizationContext`确实提供了一些重要的功能:它流动请求上下文.这包括从身份验证到Cookie到文化的各种内容.因此,在ASP.NET中,您可以同步回请求上下文,而不是同步回UI.这可能很快就会改变:新的'ApiController`确实有一个`HttpRequestMessage`上下文作为属性 - 因此**可能*不需要通过`SynchronizationContext`来传递上下文 - 但我还不知道. (3认同)
  • ASP.NET`SynchroniztaionContext`是否有一些文档解释了某些请求在上下文中只能有一个线程?如果没有,我认为应该有. (2认同)
  • 谢谢!我错误地使用了Task.WaitAll而不是等待Task.WhenAll.你的回答让我走上正轨 (2认同)

Yko*_*kok 58

编辑:一般尽量避免做以下操作,除非作为最后的努力,以避免死锁.阅读Stephen Cleary的第一条评论.

这里快速修复.而不是写:

Task tsk = AsyncOperation();
tsk.Wait();
Run Code Online (Sandbox Code Playgroud)

尝试:

Task.Run(() => AsyncOperation()).Wait();
Run Code Online (Sandbox Code Playgroud)

或者如果您需要结果:

var result = Task.Run(() => AsyncOperation()).Result;
Run Code Online (Sandbox Code Playgroud)

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

现在将在ThreadPool上调用AsyncOperation,其中不存在SynchronizationContext,并且AsyncOperation内部使用的continuation将不会强制返回到调用线程.

对我来说,这看起来像一个可用的选项,因为我没有选择让它一直异步(我更喜欢).

从来源:

确保FooAsync方法中的await没有找到要编组的上下文.最简单的方法是从ThreadPool调用异步工作,例如将调用包装在Task.Run中,例如

int Sync(){return Task.Run(()=> Library.FooAsync()).结果; }

现在将在ThreadPool上调用FooAsync,其中不存在SynchronizationContext,并且FooAsync内部使用的延续不会被强制回到调用Sync()的线程.

  • 可能想重新阅读你的源链接; 作者建议*不*这样做.它有用吗?是的,但只是在你避免死锁的意义上.这个解决方案否定了ASP.NET上"async"代码的所有好处,实际上可能会导致大规模的问题.BTW,`ConfigureAwait`在任何情况下都不会"破坏正确的异步行为"; 它正是你应该在库代码中使用的. (6认同)
  • 我喜欢这里的所有答案,并且一如既往......它们都是基于上下文(双关语).我正在使用同步版本包装HttpClient的异步调用,因此我无法更改该代码以将ConfigureAwait添加到该库.因此,为了防止生产中出现死锁,我将Async调用包装在Task.Run中.据我了解,这将为每个请求使用1个额外的线程并避免死锁.我认为要完全兼容,我需要使用WebClient的同步方法.这是很多工作要证明,所以我需要一个令人信服的理由,不要坚持我目前的方法. (3认同)
  • 这是整个第一部分,标题为粗体“避免为异步实现公开同步包装器”。这篇文章的其余部分解释了几种不同的方法来做到这一点*如果*你绝对*需要*。 (2认同)

Has*_*thi 8

由于您正在使用.Result或,.Wait否则await最终将导致代码死锁

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

像这样:

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

您可以ConfigureAwait(false)在任何情况下使用“请勿阻止异步代码”。