【问题标题】:Awaiting manually created task doesn't regard inner awaits [duplicate]等待手动创建的任务不考虑内部等待[重复]
【发布时间】:2019-10-17 19:11:10
【问题描述】:

我在使用 async 和 await 时遇到了一个奇怪的行为。如果我尝试等待手动创建的任务 T1,它本身应该等待另一个任务 T2,即使任务 T2 仍在等待,任务 T1 已经运行完成。

为了说明问题,我编写了一些代码。当我从这个例子运行'task1'时,输出是:

task1 运行完成 更新中...

当我改为运行“task2”时,输出总是:

更新中... task2 运行完成

有人解释为什么第一个等待表达式不依赖于内部等待吗?

private void OnLoaded(object sender, RoutedEventArgs e)
{
    var task1 = new Task(async () => await UpdateAfterDelay());
    task1.Start();
    await task1;
    Console.WriteLine("task1 ran to completion");

    var task2 = Task.Run(async () => await UpdateAfterDelay());
    await task2;
    Console.WriteLine("task2 ran to completion");
}

private async Task UpdateAfterDelay()
{
    await Task.Delay(2000);
    Console.WriteLine("updating...");
}

【问题讨论】:

  • 您遇到了奇怪的行为,因为您正在做非常奇怪的事情。您已经有一个返回任务的异步方法;你为什么要经历所有这些用异步 lambda 创建新任务的繁琐程序?如果您想要两个等待的两个任务,只需执行 await Update(); await Update(); 即可。你能解释一下为什么你试图构建这个可以简单表达的过于复杂和令人困惑的工作流程吗?
  • Task<TResult> 构造函数不理解异步委托。这意味着它不会将Func<Task> 识别为特殊的东西。它认为Task 只是一个正常的返回值。所以你最终会得到一个返回值Task<Task>。在这些情况下,您必须分别等待外部和内部Task 的完成,这是通过await await 实现的。因此,要使您的代码按预期运行,您必须进行两项小的更改:1)使用通用Task 构造函数(new Task<Task>)而不是非通用构造函数。 2)等待两次结果任务。
  • @Theodor Zoulias 那行得通。谢谢。但是由于使用了 await 运算符,推断的类型不是 Task 。所以我仍然对为什么这不起作用感到困惑。如果我在不使用 await 的情况下声明表达式,编译器会通知我,调用不会等待,以便继续执行。这没有任何意义,因为使用 await 会导致相同的行为。
  • 我试图将一大堆异步表达式封装在一个方法中。由于使用 async / await 需要您使整个 API 异步,因此我无法避免使这些方法异步(总而言之,我试图在不同的线程上执行计算密集型操作,并且之后必须更新我的 UI) .但不管我的代码如何,我现在对为什么这两种方法的行为不同感兴趣。由于两者都在采取行动回调,我不希望这样。如果我写“new Task(() => UpdateAfterDelay())”,省略 await 运算符会导致编译器警告。
  • 您当前正在使用非泛型Task 构造函数,它接受Action 类型的参数。不是Func。因此,您的 UpdateAfterDelay 方法的返回值将被丢弃。这成为一个问题,因为此方法的返回值为Task,而您无法等待此Task,因为未返回对它的引用。所以你的代码变得并行,被忽略的Tasktask1.Start() 之后的代码同时运行。您可能认为await task1; 等待上述任务,但事实并非如此。它等待创建被忽略的内部任务的外部任务。

标签: c# async-await task


【解决方案1】:

丹尼尔的想法是对的。如果您查看constructors for Task,它们都只接受Action 对象,它们本质上是void 方法。这意味着您的匿名方法被解释为async void,即“一劳永逸”(启动它,不要等待它)。

如果你不使用匿名方法,这会变得更清楚:

var task1 = new Task(UpdateAfterDelayTask); //this does not compile

var task2 = new Task(UpdateAfterDelayVoid); //this does

private async Task UpdateAfterDelayTask()
{
    await Task.Delay(2000);
}

private async void UpdateAfterDelayVoid()
{
    await Task.Delay(2000);
}

task1 赋值抱怨你给它的方法有错误的返回类型。

Task.Run,但是,has an overload 接受 Func<Task>(一种返回 Task 的方法)。所以编译器检查你的匿名方法,发现它返回一个Task,然后选择Func<Task>重载。那个过载returns a new Task that depends on the Task returned by your anonymous method

说了这么多,也许您使用 new TaskTask.Run 的原因是您尚未共享,但实际上,您实际上并不需要这样做。你可以这样做:

var task1 = UpdateAfterDelay();
// do something else
await task1;

如果您没有“做其他事情”,那么您根本不需要task1 变量。只需await UpdateAfterDelay()

同样值得注意的是this article,它解释了为什么你几乎不需要使用new Task(),而this later article(写在Task.Run出来之后)解释了为什么你几乎总是想使用Task.Run()(如果你甚至需要)。

【讨论】:

  • 请注意:文章“Task.Factory.StartNew” vs “new Task(…).Start” 实际上并不建议永远不要使用new Task()。文章Task.Run vs Task.Factory.StartNew 也不建议始终使用Task.Run()。两篇文章都介绍了更专业方法的可能用途。
  • @TheodorZoulias True...我澄清了那句话。
  • Stephen Cleary 的观点比 Stephen Toub about task constructors 更强烈:不要使用 Task 或 Task 构造函数。 :-)
  • 好像是这样。即使 Task.Run 也像构造函数一样接受一个 Action 对象,它也可能以不同的方式处理它。但值得注意的是,在我的示例中,没有推断出类型 Func,因为我将 Action 回调声明为异步方法。因此,如果我声明“new Task(async()=> await UpdateAfterDelayTask())”,编译器不会抱怨任何事情,并且可以预期该任务是可等待的。
  • @Dennis Kassel 匿名异步委托被解释为Func<Task>Func<Task<TResult>>。这取决于 => 之后的内容。在您的情况下,您指向 async void 方法,因此异步委托被解释为Func<Task>。当 new Task 构造函数带有 Func<Task> 参数时,允许通过丢弃返回值来像使用 Action 一样使用它。将其与这行代码进行比较:Math.Abs(0);。这是允许的。这是完全没用的,因为Math.Abs 方法没有副作用,但你可以这样做。
【解决方案2】:

我假设这里发生的情况是,传递给Task 构造函数的异步 lambda 被隐式转换为 Action,因为 Task 类不包含接受 Func<Task> 的构造函数,即你想要什么。因此它就像Action 一样执行 - 并立即返回。检查 IntelliSense 中构造函数中使用的类型以确认这一点。

【讨论】:

    【解决方案3】:

    其他答案都有正确的想法,但总结和简化:

    使用

    var task1 = new Task(...)
    task1.Start();
    

    使用异步 lambda 创建一个在后台运行并立即返回的 void Task(因此 await task1 实际上不需要等待)

    但使用

    var task2 = Task.Run(...)
    

    创建一个仍在后台运行的可等待任务,但实际上会返回一个 await 可以等待的值。

    【讨论】:

    • 试试这个代码:var t = new Task(() => Thread.Sleep(1000)); t.Start(); t.Wait(); 肯定会导致调用线程等待。
    • @TheodorZoulias .Wait() 不同于 await
    • t.Wait(); 替换为await t;。同样的事情。
    • @Lamp:对原始问题的关键是传递给Task 构造函数的委托是从async lambda 实例化的。委托调用立即返回,因为第一个(唯一)认为它确实是await。您的帖子根本没有解决这个关键方面,实际上您所写的内容不适用于该方面不成立的任何情况。我看不出这会带来什么有用的东西,更不用说会增加现有答案的东西了。
    • 添加了四个字,因为您无法弄清楚我在回答原始问题。最佳答案很好,但涵盖了更一般的情况,这使得答案更复杂。正如我所说,我的回答是为了简化这种特定情况。
    猜你喜欢
    • 1970-01-01
    • 2012-11-06
    • 1970-01-01
    • 1970-01-01
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-28
    相关资源
    最近更新 更多