等待任务结果时会发生什么?

50
我在.NET 4.0项目中使用HttpClient将数据发送到远程服务。因为我不担心此操作阻塞,所以我想跳过ContinueWith或async/await并使用Result。在调试过程中,遇到了一个问题:远程服务器没有响应。当我逐步执行代码时,似乎我的代码在第三行就停止了运行……当前堆栈指针行不再高亮显示,并且没有进入下一行。它只是消失了。花了我一段时间才意识到,我应该等待请求超时。
var client = new HttpClient();
var task = client.PostAsync("http://someservice/", someContent);
var response = task.Result;

我的理解是,在Task上调用Result会使代码以同步方式执行,更像这样(我知道HttpClient中没有Post方法):

var client = new HttpClient();
var response = client.Post("http://someservice/", someContent);

我不确定这是一件坏事,我只是试图理解它。HttpClient返回任务而不是直接结果,这样做是否真的意味着当我认为我避免了异步时,我的应用程序自动利用了异步?


文档中提到: "此属性的获取访问器确保在返回之前完成异步操作。"所以你可以正确地假设如此。然而,可能会遇到任务抛出异常的情况,因为操作失败了。 - Simon Ejsing
我猜让我感到困惑的是堆栈指针消失了。我没有意识到如果我等待足够长时间,阻塞 Result 调用将引发异常。它看起来就像代码在方法执行一半时停止了一样。也许这应该是一个 VS Connect 类型问题,要求一些 UI 提示,表明第三行有异步操作正在等待。我认为调试器甚至把我带回了调用类。 - scottt732
有没有带完整源代码的最终解决方案? - Kiquenet
2个回答

61

在Windows中,所有I/O操作都是异步的。同步API只是方便的抽象。

因此,当你使用HttpWebRequest.GetResponse时,实际发生的是I/O操作被启动(异步),调用线程(同步)会阻塞,等待它完成。

同样地,当你使用HttpClient.PostAsync(..).Result时,I/O操作被启动(异步),调用线程(同步)会阻塞,等待它完成。

我通常建议人们使用await而不是Task.ResultTask.Wait,原因如下:

  1. 如果你在一个async方法的结果上阻塞了Task,你很容易陷入死锁的情况
  2. Task.ResultTask.Wait将任何异常包装在AggregateException中(因为这些API是TPL的遗留问题)。所以错误处理更加复杂。

然而,如果你知道这些限制,有些情况下在Task上阻塞可能是有用的(例如,在控制台应用程序的Main中)。


1
那么结论是如果您的代码是同步的,就不要使用HttpClient?这不是有点.NET设计上的瑕疵吗?除非我们将整个调用堆栈设置为异步,否则无法使用异步方法。我认为等待异步方法完成以便完成同步操作应该是异步等待的最常见用法。 - Vukoje
2
@Vukoje:自然异步操作(包括所有I/O)应该由异步API表示。正确的使用方式是异步而不是同步消费它们。如果开发人员“需要”同步地使用异步API,那么这总是表明消费应用程序中存在设计错误(有时由于框架中的设计错误而不可避免,但通常只是应用程序中的设计错误)。 - Stephen Cleary
感谢澄清!只是为了确保我完全理解,如果我有一个不想使其异步的WebAPI方法,我就不应该使用HttpClient.PostAsync吗?如果在那个WebAPI方法(非异步)中,我想要进行两个并行的HTTP post怎么办?在这种情况下,我应该使用两个PostAsync()调用与Task.WaitAll相结合的方法,还是这种方法也不好? - Vukoje
1
@Vukoje:如果您的WebAPI方法正在执行HTTP请求,则应该是异步的。如果出于任何原因选择使其同步,则可以在每个(同步)POST调用中包装一个Task.Run - Stephen Cleary
非常感谢您的努力,它确实帮了我很多!但我仍然不太明白为什么要将WebApi方法设置为异步。这会增加一些复杂性,而我通过阅读发现唯一的好处可能是在高负载下有助于提高Web服务器的响应时间。 - Vukoje
@Vukoje:是的,可扩展性是主要优点。我还发现它更易于维护,因为异步方法是异步的。 - Stephen Cleary

4

捕获任务的结果会阻塞当前线程。在这种情况下,使用方法的异步版本是没有意义的。Post()PostAsync().Result都会阻塞。

如果您想利用并发性,应该这样编写:

async Task PostContent()
{
  var client = new HttpClient();
  Task t = await client.PostAsync("http://someservice/", someContent);
  //code after this line will execute when the PostAsync completes.
  return t;
}

由于PostContent()本身返回一个Task,调用该方法的方法也应该等待。

async void ProcessResult()
{
   var result = await PostContent(); 
   //Do work with the result when the result is ready 
}

例如,如果你在按钮点击处理函数中调用ProcessResult(),你会发现UI仍然响应,其他控件仍然能够正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接