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

时间:2012-04-27 01:28:08

标签: c# .net asynchronous async-await dotnet-httpclient

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

修改:在测试案例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

此处的每个端点都返回相同的数据(来自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();
    }
}

8 个答案:

答案 0 :(得分:431)

您滥用API。

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

这是managed by the 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会返回未完成的Task
  • Test5Controller.Get 阻止当前线程,直到Task完成。
  • HTTP响应进来,Task返回的HttpClient.GetAsync已完成。
  • AsyncAwait_GetSomeDataAsync尝试在ASP.NET请求上下文中继续。但是,在该上下文中已经存在一个线程:Test5Controller.Get中阻塞的线程。
  • 死锁。

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

  • test1test2test3):Continuations_GetSomeDataAsync安排线程池的延续,外部 ASP.NET请求上下文。这允许Task返回的Continuations_GetSomeDataAsync完成,而无需重新输入请求上下文。
  • test4test6):由于Task 等待,因此不会阻止ASP.NET请求线程。这允许AsyncAwait_GetSomeDataAsync在准备好继续时使用ASP.NET请求上下文。

这是最好的做法:

  1. 在你的&#34;图书馆&#34; async方法,尽可能使用ConfigureAwait(false)。在您的情况下,这会将AsyncAwait_GetSomeDataAsync更改为var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. 不要阻挡Task;它async一路向下。换句话说,使用await代替GetResultTask.ResultTask.Wait也应替换为await)。
  3. 这样,您可以获得两个好处:延续(AsyncAwait_GetSomeDataAsync方法的其余部分)在基本线程池线程上运行,该线程不必进入ASP.NET请求上下文;控制器本身是async(它不会阻止请求线程)。

    更多信息:

    更新2012-07-13:纳入此答案into a blog post

答案 1 :(得分:56)

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

here快速修复。而不是写:

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(){       return Task.Run(()=&gt; Library.FooAsync())。结果; }

     

现在将在ThreadPool上调用FooAsync,而不会有   SynchronizationContext,以及FooAsync内部使用的延续   不会被强制回到调用Sync()的线程。

答案 2 :(得分:4)

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

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

像这样:

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

您可以尽可能使用ConfigureAwait(false)“禁止阻止异步代码”。

答案 3 :(得分:1)

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

以下是您只需使用

的情况
   Task.Run(() => AsyncOperation()).Wait(); 

或类似

   AsyncContext.Run(AsyncOperation);

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

我需要的库是异步的,因为它应该运行异步。

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

我只是对每一个人说。

答案 4 :(得分:1)

在我的情况下,“ await”从未执行完,因为执行请求时出现异常,例如服务器没有响应,等等。用try..catch包围它以标识发生了什么,它还会优雅地完成您的“等待”。

public async Task<Stuff> GetStuff(string id)
{
    string path = $"/api/v2/stuff/{id}";
    try
    {
        HttpResponseMessage response = await client.GetAsync(path);
        if (response.StatusCode == HttpStatusCode.OK)
        {
            string json = await response.Content.ReadAsStringAsync();
            return JsonUtility.FromJson<Stuff>(json);
        }
        else
        {
            Debug.LogError($"Could not retrieve stuff {id}");
        }
    }
    catch (Exception exception)
    {
        Debug.LogError($"Exception when retrieving stuff {exception}");
    }
    return null;
}

答案 5 :(得分:0)

我在这里看:

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版本有效,并且是“正确”的做事方式,您真的需要回答这个问题吗?

我的投票是:滥用API

答案 6 :(得分:0)

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

最后发现我忘记了在调用堆栈的更深处await进行async的呼叫。

感觉和分号一样好。

答案 7 :(得分:0)

我使用了很多等待,所以我没有得到响应,我转换为同步调用它开始工作

            using (var client = new HttpClient())
            using (var request = new HttpRequestMessage())
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                request.Method = HttpMethod.Get;
                request.RequestUri = new Uri(URL);
                var response = client.GetAsync(URL).Result;
                response.EnsureSuccessStatusCode();
                string responseBody = response.Content.ReadAsStringAsync().Result;