使用await / async时,HttpClient.GetAsync(...)从不返回


315

编辑: 这个问题看起来可能是相同的问题,但没有任何响应...

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

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

仅在使用新的异步/等待语言功能和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();
    }
}

2
它不会出现相同的问题,但只是为了确保你了解它,有一个在测试WRT异步方法的MVC4错误,同步完成-见stackoverflow.com/questions/9627329/...
詹姆斯曼宁

谢谢-我会注意的。在这种情况下,我认为由于对HttpClient.GetAsync(...)?的调用,该方法应始终是异步的。
本杰明·福克斯

Answers:


468

您正在滥用API。

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

由ASP.NET管理SynchronizationContext

默认情况下,当你await一个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返回uncompleted Task
  • Test5Controller.Get 阻塞当前线程,直到Task完成为止。
  • HTTP响应进入,Task返回的HttpClient.GetAsync完成。
  • AsyncAwait_GetSomeDataAsync尝试在ASP.NET请求上下文中恢复。但是,在这种情况下已经有一个线程:该线程在中被阻塞Test5Controller.Get
  • 僵局。

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

  • test1test2test3):在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. 不要阻塞Tasks;它是async一路下滑。换句话说,请使用await代替GetResultTask.Result并且Task.Wait也应替换为await)。

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

更多信息:

2012年7月13日更新:将此答案纳入博客文章中


2
是否有一些ASP.NET文档SynchroniztaionContext说明了某些请求在上下文中只能有一个线程?如果没有,我认为应该有。
svick

8
在AFAIK的任何地方都没有记录。
斯蒂芬·克莱里

10
谢谢- 非常棒的回复。(显然)功能相同的代码之间在行为上的差异令人沮丧,但对于您的解释是有意义的。如果框架能够检测到此类死锁并在某处引发异常,则将非常有用。
本杰明·福克斯

3
是否存在不建议在asp.net上下文中使用.ConfigureAwait(false)的情况?在我看来,应该始终使用它,并且仅在UI上下文中才应该使用它,因为您需要同步到UI。还是我错过了重点?
AlexGad

3
ASP.NET SynchronizationContext确实提供了一些重要的功能:它流动请求上下文。这包括从身份验证到Cookie到文化的各种内容。因此,在ASP.NET中,您无需同步回UI,而是同步回到请求上下文。这可能会很快改变:新ApiController的确具有HttpRequestMessage上下文作为属性-因此可能不需要上下文来传递SynchronizationContext-但我还不知道。
史蒂芬·克雷里

61

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

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

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

现在,将在ThreadPool上调用FooAsync,在该线程池中将没有SynchronizationContext,并且在FooAsync内部使用的继续操作不会被强制返回到调用Sync()的线程。


7
可能想重新阅读您的源链接;作者建议要这样做。它行得通吗?是的,但是只能避免死锁。此解决方案抵消了ASP.NET 上代码的所有好处,async实际上可能会导致大规模问题。顺便说一句,ConfigureAwait在任何情况下都不会“破坏适当的异步行为”;这正是您应该在库代码中使用的。
Stephen Cleary

2
这是整个第一部分,以粗体显示Avoid Exposing Synchronous Wrappers for Asynchronous Implementations如果您绝对需要,本文章的其余部分将说明几种不同的方法。
Stephen Cleary

1
添加了我在源代码中找到的部分-我将由以后的读者决定。请注意,通常应该避免这样做,而只能将其作为最后的手段(例如,使用异步代码时,您无法控制)。
Ykok 2013年

3
我喜欢这里的所有答案,而且一如既往地...。它们都是基于上下文的(双关语,哈哈)。我将HttpClient的Async调用包装为同步版本,因此无法更改该代码以将ConfigureAwait添加到该库。因此,为了避免生产中的死锁,我将Async调用包装在Task.Run中。因此,据我了解,这将为每个请求使用1个额外的线程,并避免了死锁。我假设要完全兼容,我需要使用WebClient的sync方法。有很多工作需要证明,因此我需要一个令人信服的理由,不要坚持我目前的做法。
samneric

1
我最终创建了一个扩展方法来将异步转换为同步。我在这里读到的内容与.Net框架的用法相同:public static TResult RunSync <TResult>(this Func <Task <TResult >> func){return _taskFactory .StartNew(func).Unwrap().GetAwaiter() .GetResult(); }
samneric's

10

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

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

像这样:

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

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


2

这两所学校并没有真正排除在外。

这是您只需要使用的情况

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

或类似的东西

   AsyncContext.Run(AsyncOperation);

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

我需要的库是异步的,因为它有望运行异步。

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

我只是对每个人说。


所以您建议答案中的第一个选项?
唐·奇德尔

1

我将其放在此处的目的更多是为了完整性,而不是直接与OP相关。我花了将近一天的时间调试HttpClient请求,想知道为什么我再也回不到响应。

最终发现我忘awaitasync继续调用堆栈。

感觉就像缺少分号一样好。


-1

我在这里看:

http://msdn.microsoft.com/zh-cn/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

和这里:

http://msdn.microsoft.com/zh-cn/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

并看到:

此类型及其成员旨在供编译器使用。

考虑到await版本是可行的,并且是“正确”的处理方式,您真的需要这个问题的答案吗?

我的投票是:滥用API


我没有注意到这一点,尽管我看到了其他语言,这表明使用GetResult()API是受支持的(并且是预期的)用例。
本杰明·福克斯

1
除此之外,如果您Test5Controller.Get()通过以下方式重构以消除等待者:var task = AsyncAwait_GetSomeDataAsync(); return task.Result;可以观察到相同的行为。
本杰明·福克斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.