关于c#:HttpClient.GetAsync(…)在使用await / async时永远不会返回

HttpClient.GetAsync(…) never returns when using await/async

编辑:这个问题看起来可能是同一个问题,但没有回答…

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

我在.NET 4.5中使用system.net.http.httpclient时遇到了一些奇怪的行为,在这种情况下,"等待"对(例如)httpClient.GetAsync(...)的调用的结果将永远不会返回。

只有在使用新的异步/等待语言功能和任务API时,才会出现这种情况——当只使用延续时,代码似乎总是可以工作的。

下面是一些重现问题的代码-将其放到Visual Studio 11中的新"MVC 4 WebAPI项目"中,以公开以下get端点:

1
2
3
4
5
6
/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

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

我是否在httpclient类中遇到了错误,或者我在某种程度上使用了API?

要复制的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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();
    }
}


您滥用了API。

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

这是由ASP.NET SynchronizationContext管理的。

默认情况下,当您使用await一个Task时,该方法将在一个捕获的SynchronizationContext上恢复(如果没有SynchronizationContext的话,则在一个捕获的TaskScheduler上恢复)。通常情况下,这正是您想要的:异步控制器操作将需要cx1〔0〕一些东西,当它恢复时,它将与请求上下文一起恢复。

所以,这就是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响应进入,HttpClient.GetAsync返回的Task完成。
  • AsyncAwait_GetSomeDataAsync尝试在ASP.NET请求上下文中恢复。但是,该上下文中已经有一个线程:该线程在Test5Controller.Get中被阻塞。
  • 死锁。

这就是为什么其他方法有效:

  • (test1test2test3Continuations_GetSomeDataAsync在ASP.NET请求上下文之外,将继续调度到线程池。这使得由Continuations_GetSomeDataAsync返回的Task可以在不必重新进入请求上下文的情况下完成。
  • (test4test6:由于Task等待,ASP.NET请求线程没有被阻塞。这允许AsyncAwait_GetSomeDataAsync在准备好继续时使用ASP.NET请求上下文。

以下是最佳实践:

  • 在您的"库"async方法中,尽可能使用ConfigureAwait(false)。在你的情况下,这会把AsyncAwait_GetSomeDataAsync改为var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  • 不要挡在Task上,一路往下就是async。换言之,使用await代替GetResult(Task.Result),Task.Wait也应替换为await
  • 这样,您就得到了两个好处:继续(AsyncAwait_GetSomeDataAsync方法的其余部分)是在基本线程池线程上运行的,该线程不必进入ASP.NET请求上下文;控制器本身是async(它不阻塞请求线程)。

    更多信息:

    • 我的async/await介绍帖子,其中简要介绍了Task等待者如何使用SynchronizationContext
    • Async/Await常见问题解答,将详细介绍上下文。还可以看到等待、用户界面和死锁!哦,我的天哪!尽管您是在ASP.NET中而不是在UI中,但这里确实适用,因为ASP.NET SynchronizationContext一次只将请求上下文限制为一个线程。
    • 此msdn论坛帖子。
    • StephenToub演示了这个死锁(使用UI),LucianWischik也演示了这个死锁。

    更新2012-07-13:将此答案并入博客帖子。


    编辑:通常尽量避免做下面的工作,除了最后一点努力避免死锁。阅读斯蒂芬·克利里的第一条评论。

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

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

    尝试:

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

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

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

    从源(编辑以匹配上述示例):

    AsyncOperation will now be invoked on the ThreadPool, where there
    won’t be a SynchronizationContext, and the continuations used inside
    of AsyncOperation won’t be forced back to the invoking thread.

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

    来源:

    Ensure that the await in the FooAsync method doesn’t find a context to
    marshal back to. The simplest way to do that is to invoke the
    asynchronous work from the ThreadPool, such as by wrapping the
    invocation in a Task.Run, e.g.

    int Sync() {
    return Task.Run(() => Library.FooAsync()).Result; }

    FooAsync will now be invoked on the ThreadPool, where there won’t be a
    SynchronizationContext, and the continuations used inside of FooAsync
    won’t be forced back to the thread that’s invoking Sync().


    由于您使用的是.Result.Waitawait,这将导致代码死锁。

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

    这样地:

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

    you can use ConfigureAwait(false) wherever possible for Don't Block Async Code .


    这两所学校并不排除在外。

    以下是您只需使用的场景

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

    或者类似的

    1
       AsyncContext.Run(AsyncOperation);

    我有一个MVC操作,它在数据库事务属性下。这个想法是(很可能)如果出了什么问题,就把行动中所做的一切都回滚。这不允许上下文切换,否则事务回滚或提交本身将失败。

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

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

    我只是对每个人说自己的话。


    我将把它放在这里更多的是为了完整性而不是与操作直接相关。我花了将近一天的时间调试了一个HttpClient请求,想知道为什么我一直没有得到回复。

    最后发现我忘了把await调用放在调用堆栈的后面。

    感觉就像丢了一个分号一样好。


    我看这里:

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

    这里:

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

    看到:

    This type and its members are intended for use by the compiler.

    考虑到await版本是有效的,并且是正确的做法,您真的需要回答这个问题吗?

    我的投票是:滥用API。