【问题标题】:What does accessing a Result on a Task before calling Wait actually do?在调用 Wait 之前访问任务上的结果实际上是做什么的?
【发布时间】:2020-02-05 12:33:32
【问题描述】:

var task = Task.Run(() => DoSomeStuff()).Result;

引擎盖下发生了什么?

我做了一个小测试:

using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        var r = Task.Run( () => {Thread.Sleep(5000); return 123; }).Result;
        Console.WriteLine(r);
    }
}

它在 5 秒后打印“123”。那么访问Task 上的任何此类属性是否可以作为调用Task.Wait() 的快捷方式,即这样做安全吗?


以前我的代码称为Task.Delay(5000),它立即返回“123”。我在我的问题中解决了这个问题,但把它留在这里,因为 cmets 和答案引用它。

【问题讨论】:

  • 它就像调用了.Wait() 一样阻塞。 Task.Delay() 的测试永远不会等待。不,访问任何属性都不是Wait() 的快捷方式。 ResultWait() 的行为定义明确 - 当我们不想要任何结果时,Wait() 会阻塞。 Result 阻塞并返回结果
  • 尚未在答案中提出的重点是:除非您知道任务已经完成,否则切勿使用 Wait 或 Result。很容易陷入这样一种情况,即任务已将工作安排到您刚刚进入休眠状态的线程上,而现在任务永远不会完成,线程永远不会唤醒,因为将任务的工作一直在睡觉,直到完成!异步被设计为异步;强制它同步是危险的。
  • @EricLippert 只等待已经发生的事情?这似乎相当不直观!
  • 该评论确实应该是一个答案。将添加一个。

标签: c# task-parallel-library


【解决方案1】:

你问了两个问题。首先,访问Result 是否会隐式导致同步Wait?是的。但更重要的问题是:

这样做安全吗?

这样做不安全。

很容易陷入这样一种情况,即您正在同步等待的任务已安排工作在它完成之前在未来运行到您刚刚放入的线程上睡觉。现在我们遇到了这样一种情况,即在 sleeping 线程完成一些工作之前线程不会唤醒,而它永远不会这样做因为它处于睡眠状态

如果您已经知道任务已完成,那么同步等待结果是安全的。如果不这样做,那么同步等待是不安全的

现在,您可能会说,假设我通过其他方式知道同步等待不完整的任务是安全的。那么等待安全吗?嗯,根据问题的假设,是的,但等待仍然可能不是聪明。请记住,异步的全部意义在于在高延迟的世界中有效地管理资源。如果您正在同步等待异步任务完成,那么您将强制一个工作人员进入睡眠状态,直到另一个工作人员完成;睡觉的工人可能正在工作!异步的全部意义在于避免工人空闲的情况,所以不要强迫他们。

await 是一个异步等待。这是一个等待,意思是“等待运行当前工作流的其余部分,直到完成此任务,但在等待时找到一些事情要做”。我们做了很多工作来将它添加到语言中,所以请使用它!

【讨论】:

  • 我仍在努力解决这个问题,是否可以举一个简单的例子来说明我如何轻松进入您所描述的情况?
  • @Mr.Boy:绝对有。 Stephen 在这里举了一些例子:blog.stephencleary.com/2012/07/dont-block-on-async-code.html 一般来说,如果你想对异步中的不良模式做出有力的解释,Stephen 应该是你的首选。
  • @Mr.Boy async/await 是一项具有简单界面的复杂技术。已投入大量精力,以尽可能少的努力使其正常运行。只是避免所有花哨的东西。避免async void,避免WaitResult,避免ContinueWith,避免Task.Run,避免火而忘记任务,忘记TaskSchedulers,只专注于在任何地方使用普通的香草异步/等待平台为您提供了一个内置的异步方法。您会注意到正确的事情会自动发生。
  • @Mr.Boy:你是 100% 正确的,你不应该使用这个锋利的工具,直到你对它有很好的理解。你 100% 错误地认为在这种情况下尝试强制异步工作流变为同步是正确的做法。了解异步是如何工作的,然后接受它;不要用锤子来强迫它像同步代码一样工作。
  • @Mr.Boy 如果您对老式的延续 (ContinueWith) 感到满意并且对 async/await 魔法感到不舒服,那么您无论如何都可以继续使用旧的东西。他们仍然像以往一样工作。只是避免将它们与新东西混合。将经典的 TPL 技术与 async/await 结合起来会导致挫折和问题。
【解决方案2】:

那么访问Task 上的任何此类属性是否可以作为调用Task.Wait() 的快捷方式?

是的。

来自the docs

访问[Result]属性的get访问器会阻塞调用线程,直到异步操作完成;相当于调用Wait方法。

但是,您的测试并没有按照您的想法进行。

Task.Delay(..) 返回一个Task,它在指定的时间后完成。它不会阻塞调用线程。

所以() => { Task.Delay(5000); return 123; } 只需创建一个新的Task(将在 5 秒内完成),然后将其丢弃并立即返回123

您可以:

  1. 通过执行Task.Delay(5000).Wait()(与Thread.Sleep(5000) 执行相同的操作)阻止调用线程
  2. 异步等待从Task.Delay返回的Task完成:Task.Run(async () => { await Task.Delay(5000); return 123; })

【讨论】:

  • 一个简洁的功能,然后作为 1-liner 运行和等待任务。谢谢你的好回答
  • 没错,它主要是为了建立一个Task 导向的代码库和简单的案例测试。我试图思考它是否有任何不同?
【解决方案3】:

测试不会等待Task.Delay(),所以它会立即返回。应该是:

var r = Task.Run(async  () => { await Task.Delay(5000); return 123; }).Result;

Result 的行为是明确定义的——如果任务没有完成,它会阻塞直到完成。访问其他任务属性不会阻塞

【讨论】:

  • 是的,我只是想制作一个 Task 来模拟需要几秒钟的东西。
  • @Mr.Boy:除了模拟工作的测试用例之外,永远不要使用非零参数调用Thread.Sleep,就像你在这里所做的那样。当然永远不要在生产代码中使用它来造成延迟。如果你在一个真正的办公室里有一个真正的工作人员的工作流程,你永远不会说“这封邮件应该在下个月的第一天发出。玛丽,睡着了,设置下个月的闹钟,然后发送邮件;我们会在你睡觉的时候继续支付你的工资和福利”。工人是昂贵的资源,异步的全部意义在于让他们睡觉!
  • @EricLippert:我见过一些人(例如Joe Duffy)为此目的推荐Thread.Sleep(1) 而不是Thread.Sleep(0)。不同之处在于Thread.Sleep(1) 无条件地从调度程序中删除线程,允许其他线程的时间片,即使这些线程具有较低的优先级。
  • @Brian:是的,“永远听从 Joe 的建议”是我进行多线程编程的基本规则之一,就在“如果可以避免就不要这样做”之后。 :)
猜你喜欢
  • 2011-03-11
  • 2010-11-26
  • 2015-06-23
  • 2017-12-26
  • 2016-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多