一尘不染

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

c#

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

编辑: 在测试用例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();
    }
}

阅读 660

收藏
2020-05-19

共1个答案

一尘不染

您正在滥用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返回一个uncompleted 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调度到线程池的继续。这使得返回者可以完成而不必重新输入请求上下文。 __Task``Continuations_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日更新: 将此答案纳入博客文章中

2020-05-19