【问题标题】:Use an async callback with Task.ContinueWith使用带有 Task.ContinueWith 的异步回调
【发布时间】:2013-09-12 21:06:00
【问题描述】:

我正在尝试使用 C# 的 async/await/continuewith。 我的目标是必须有 2 个并行运行的任务,尽管哪个任务按顺序执行一系列动作。 为此,我计划有一个 List<Task> 代表并行运行的 2 个(或更多)任务,并在每个 Task 上使用 ContinueWith 我的问题是,await taskList 已经返回时,continue 中的回调似乎没有被执行。

为了总结,这里有一个示例来说明我期望发生的事情:

class Program
{
    static public async Task Test()
    {
        System.Console.WriteLine("Enter Test");
        await Task.Delay(100);
        System.Console.WriteLine("Leave Test");
    }

    static void Main(string[] args)
    {
        Test().ContinueWith(
        async (task) =>
        {
            System.Console.WriteLine("Enter callback");
            await Task.Delay(1000);
            System.Console.WriteLine("Leave callback");
        },
        TaskContinuationOptions.AttachedToParent).Wait();
        Console.WriteLine("Done with test");
    }
}

预期的输出是

Enter Test
Leave Test
Enter callback
Leave callback
Done with test

但是,输出是

Enter Test
Leave Test
Enter callback
Done with test

有没有办法让调用ContinueWith 的任务在被视为完成之前等待提供的函数完成? IE。 .Wait 将等待两个任务完成,原始任务和 ContinueWith 返回的任务

【问题讨论】:

  • 尝试从await Task.Delay(1000);中删除await
  • 确实有效。我删除了等待和异步,因为没有等待。但是我错过了它工作的原因。你有理论上的解释吗?
  • “Done with test”后是否出现“离开回调”?
  • 调用task.ContinueWith(async (task) => { .. })实际上返回Task,更多关于包装任务here。这正是您想要的吗?
  • 我会避免在不必要的情况下使用包装任务,here's 我的意思。

标签: c# async-await


【解决方案1】:

在进行async 编程时,应努力将ContinueWith 替换为await,如下所示:

class Program
{
  static public async Task Test()
  {
    System.Console.WriteLine("Enter Test");
    await Task.Delay(100);
    System.Console.WriteLine("Leave Test");
  }

  static async Task MainAsync()
  {
    await Test();
    System.Console.WriteLine("Enter callback");
    await Task.Delay(1000);
    System.Console.WriteLine("Leave callback");
  }

  static void Main(string[] args)
  {
    MainAsync().Wait();
    Console.WriteLine("Done with test");
  }
}

使用await 的代码更简洁,更易于维护。

此外,您不应将父/子任务与async 任务一起使用(例如,AttachedToParent)。它们不是为协同工作而设计的。

【讨论】:

  • 如果我想触发并忘记一个异步方法,但仍然捕获和异常,所以我可以记录它怎么办?
  • “一劳永逸”是有问题的。您关心异常的事实表明它并不是真正的“一劳永逸”(因为它没有被遗忘)。正确的“即发即弃”解决方案取决于代码到底在做什么。
  • 嗨斯蒂芬,在我的情况下,我很高兴有一个例外,只要我可以记录它。但是,我对等待这个漫长的手术结束并不满意。
  • @niproblema:“只要我能记录下来”不是“忘记”。在这种情况下,the proper solution for request-extrinsic code is a durable queue with a background service.
【解决方案2】:

我想添加我的答案来补充已被接受的答案。根据您尝试执行的操作,通常可以避免增加异步委托和包装任务的复杂性。例如,您的代码可以像这样重构:

class Program
{
    static async Task Test1()
    {
        System.Console.WriteLine("Enter Test");
        await Task.Delay(100);
        System.Console.WriteLine("Leave Test");
    }

    static async Task Test2()
    {
        System.Console.WriteLine("Enter callback");
        await Task.Delay(1000);
        System.Console.WriteLine("Leave callback");
    }

    static async Task Test()
    {
        await Test1(); // could do .ConfigureAwait(false) if continuation context doesn't matter
        await Test2();
    }

    static void Main(string[] args)
    {
        Test().Wait();
        Console.WriteLine("Done with test");
    }
}

【讨论】:

  • 在被这种Task<Task> 行为和对晦涩的Unwrap() 的需求所困扰之后,我将返回并重构为简单的代码块而不是延续。但是,值得注意的是,a) 来自优秀 .Net 开发人员的数百个示例使用 continuation,几乎没有 Unwrap 和 b) docs 自己将 Continuation 宣传为“相对易于​​使用,但功能强大且灵活”。这个特别晦涩的角落案例有点令人沮丧......
  • @mdisibio,有很多简单的单行代码,ContinueWith + Unwrap 是合理的。使用此组合,可以轻松正确地传播取消状态。例如:stackoverflow.com/a/62607500/1768303
  • lol Unwrap() 到 ContinueWith 正在演变为 ConfigureAwait(false) 到 await - 易于添加,当您忘记添加时很烦人!
  • @mdisibio,更像是忘记了await 本身:) 至于ConfigureAwait(false),如今它与IMO 的相关性要低得多。我的(不受欢迎的)意见是to not dogmatically use it。
  • @mdisibio Continuations That Return Task Types (来自关于延续的 MS 概述文档)具体描述了何时以及为何使用 Unwrap()。该页面上其他部分的示例也使用 Unwrap 进行所有异步延续。
【解决方案3】:

当使用ContinueWith 方法链接多个任务时,您的返回类型将为Task<T>,而T 是传递给ContinueWith 的委托/方法的返回类型。

由于异步委托的返回类型是Task,您最终会得到Task<Task>,并最终等待异步委托返回Task,这是在第一个await 之后完成的.

为了纠正这种行为,您需要使用返回的Task,嵌入在您的Task<Task> 中。使用Unwrap扩展方法解压。

【讨论】:

  • 将近 7 年后,可以确认这是必要的步骤,而不是我在 MS 文档中的任何地方都找不到的。我使用 ContinueWith 作为穷人的工作队列,奇怪的是,queue = queue.ContinueWith(_ => asyncFunc()); 在 Windows 下似乎按预期工作,在 Linux 上却没有,你肯定需要queue.ContinueWith(_ => asyncFunc()).Unwrap();。那里的关键词是“似乎”——即使在没有 Unwrap() 的 Windows 下,它实际上也不会按顺序运行任务,但它们之间有足够的延迟使其能够很好地工作。
  • @DylanNicholson 不开玩笑!我在一台 Windows 机器上做了几十次测试,我精心设计的“Continuations”似乎运行良好。直到我将相同的代码部署到基于 Linux 的 Docker 容器之前,我才完全意识到需要 Unwrap。
  • 还有;使用通用 ContinueWith<> 可以帮助防止编译时出现意外行为,例如如果Continuation 是async 并返回Task<ExpectedResult> 而不是ExpectedResult,task.ContinueWith<ExpectedResult>(Continuation) 将无法编译,而不是静默编译但行为异常。
猜你喜欢
  • 1970-01-01
  • 2018-04-29
  • 1970-01-01
  • 1970-01-01
  • 2016-09-14
  • 2015-01-20
  • 2022-01-22
  • 2016-10-24
  • 1970-01-01
相关资源
最近更新 更多