首页 文章

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

提问于
浏览
270

Edit: This question看起来可能是同样的问题,但没有回复......

Edit: 在测试用例5中,任务似乎停留在 WaitingForActivation 状态 .

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

这仅在使用新的异步/等待语言功能和任务API的某些情况下发生 - 当仅使用延续时,代码似乎始终有效 .

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

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

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

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

Code to reproduce:

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

5 回答

  • -1

    您滥用API .

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

    这是managed by the ASP.NET SynchronizationContext .

    默认情况下,当您 await Task 时,该方法将在捕获的 SynchronizationContext (或捕获的 TaskScheduler ,如果没有 SynchronizationContext )上恢复 . 通常情况下,这正是您想要的:异步控制器操作将会发生一些事情,当它恢复时,它将继续执行请求上下文 .

    所以,这就是 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 blocks 当前线程直到 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请求上下文 .

    这是最好的做法:

    • 在"library" async 方法中,尽可能使用 ConfigureAwait(false) . 在你的情况下,这将改变 AsyncAwait_GetSomeDataAsyncvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);

    • 不要阻止 Task ;它一直都是 async . 换句话说,使用 await 而不是 GetResultTask.ResultTask.Wait 也应该用 await 替换) .

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

    更多信息:

    Update 2012-07-13: 收纳了这个答案into a blog post .

  • 407

    由于您使用的是 .Result.Waitawait ,因此最终会导致代码中出现 deadlock .

    你可以在 async 方法中使用 ConfigureAwait(false) 来实现 preventing deadlock

    像这样:

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

    您可以尽可能使用ConfigureAwait(false)来阻止异步代码 .

  • 3

    这两所学校并没有真正排除 .

    这是您只需使用的场景

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

    或类似的东西

    AsyncContext.Run(AsyncOperation);
    

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

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

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

    我只是对每一个人说 .

  • 52

    我在这看:

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

    和这里:

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

    并看到:

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

    考虑 await 版本有效,并且是'right'做事的方式,你真的需要这个问题的答案吗?

    我的投票是: Misusing the API .

  • 0

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

    here快速修复 . 而不是写:

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

    尝试:

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

    或者如果您需要结果:

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

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

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

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

    从来源:

    确保FooAsync方法中的await没有找到要编组的上下文 . 最简单的方法是从ThreadPool调用异步工作,例如将调用包装在Task.Run中,例如, int Sync(){return Task.Run(()=> Library.FooAsync()) . 结果;现在将在ThreadPool上调用FooAsync,其中不存在SynchronizationContext,并且FooAsync内部使用的延续不会被强制回到调用Sync()的线程 .

相关问题