提问者:小点点

使用Await/Async时,HttpClient.GetAsync(…)从不返回


编辑:此问题看起来可能是同一个问题,但没有响应。。。

编辑:在测试用例5中,任务似乎被困在waitingforactivation状态中。

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

只有在使用新的Async/Await语言功能和Tasks API的特定情况下,才会出现这种情况--当只使用continuations时,代码似乎总是能正常工作。

下面是一些再现问题的代码--将其放到Visual Studio 11中的一个新的“MVC4 WebApi项目”中,以公开以下GETendpoint:

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

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

要复制的代码:

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

共3个答案

匿名用户

您正在误用API。

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

这是由ASP.NETSynchronizationContext管理的。

默认情况下,当等待一个任务时,该方法将在捕获的SynchronizationContext(或者捕获的TaskScheduler(如果没有SynchronizationContext))上恢复。通常,这正是您想要的:一个异步控制器动作将等待某个东西,当它恢复时,它将用请求上下文恢复。

下面是test5失败的原因:

  • Test5Controller.get执行AsyncaWait_GetSomeDataAsync(在ASP.NET请求上下文中)。
  • AsyncaWait_GetSomeDataAsync执行HttpClient.GetAsync(在ASP.NET请求上下文中)。
  • 发送HTTP请求,HttpClient.GetAsync返回未完成的任务
  • AsyncaWait_GetSomeDataAsync等待任务;由于未完成,ASYNCAWAIT_GetSomeDataAsync返回未完成的任务
  • Test5Controller.Get将阻塞当前线程,直到该任务完成。
  • HTTP响应进入,HttpClient.GetAsync返回的任务完成。
  • ASYNCAWAIT_GetSomeDataAsync尝试在ASP.NET请求上下文中恢复。但是,上下文中已经有一个线程:test5controller.get.
  • 中阻塞线程
  • 死锁。

以下是其他方法起作用的原因:

  • (test1test2test3):continuations_GetSomeDataAsync在ASP.NET请求上下文之外将继续调度到线程池。这允许Continuations_GetSomeDataAsync返回的任务完成,而不必重新输入请求上下文。
  • (test4test6):由于等待任务,因此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. 不阻止任务;它一直是异步。换句话说,使用Await而不是GetResult(Task.ResultTask.Wait也应替换为Await)。

这样,您就获得了两个好处:继续(AsyncAWAIT_GetSomeDataAsync方法的剩余部分)在基本线程池线程上运行,该线程不必进入ASP.NET请求上下文;控制器本身是async(它不阻塞请求线程)。

更多信息:

  • 我的异步/等待介绍文章,其中包括任务等待者如何使用synchronizationContext的简要说明。
  • async/await FAQ,它更详细地介绍了上下文。另请参阅等待,UI和死锁!哦,天哪!即使您是在ASP.NET而不是UI中,这也适用于此,因为ASP.NETsynchronizationContext将请求上下文限制为一次仅有一个线程。
  • 此MSDN论坛帖子。
  • Stephen Toub演示了这个死锁(使用UI),Lucian Wischik也演示了

更新2012-07-13:将此答案合并到一篇博客文章中。

匿名用户

编辑:一般来说,尽量避免做下面的事情,除非是为了避免死锁而做的最后努力。阅读斯蒂芬·克利里的第一篇评论。

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

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

尝试:

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

或者如果您需要一个结果:

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

来自来源(经过编辑以匹配上面的示例):

现在将在线程池上调用AsyncOperation,在线程池中不会有SynchronizationContext,并且AsyncOperation内部使用的延续不会被强制返回到调用线程。

对我来说,这看起来是一个有用的选项,因为我没有让它一路异步的选项(我更喜欢这样)。

从来源来看:

确保FooAsync方法中的await找不到要封送回的上下文。最简单的方法是从线程池调用异步工作,例如通过将调用包装在task.run中,例如。

int Sync(){return task.run(()=>Library.FooAsync())。result;}

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

匿名用户

由于您使用的是.result.waitawait,这将最终导致代码中的死锁。

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

像这样:

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

您可以尽可能使用configureAwait(false)来实现Don't Block异步代码。