【问题标题】:Why does this Task return early? Have I done something wrong?为什么这个任务提前返回?我做错了什么吗?
【发布时间】:2018-05-16 03:59:10
【问题描述】:

我正在尝试以最小的耦合设置一群工人,但我想使用 C# async 和任务。并非所有任务都是纯异步的(有些是完全同步的)。这样做的动机是我想创建一些执行业务逻辑的简单方法,并使用System.Threading.Tasks.Task API 将它们链接在一起以保留一些排序概念。基本上,我想创建第一个任务,注册一些延续,然后等待最终任务完成。

这是我构建的简单原型,只是为了看看我想做的事情是否有效:

void Main()
{
    var worker = new Worker();

    var work = worker.StartWork(1, 2000);
    work.ConfigureAwait(false);

    var final = work.ContinueWith(_ => worker.StartWork(2, 0))
        .ContinueWith(ant => worker.StartWork(3, 1500));

    var awaiter = final.ContinueWith(_ => Tuple.Create(_.Id, _.Status));
    Console.WriteLine("\"final\" completed with result {0}", awaiter.Result);
    Console.WriteLine("Done.");
}

// Define other methods and classes here
class Worker {
    internal async Task StartWork(int phase, int delay) {
        Console.WriteLine("Entering phase {0} (in Task {1}) with {2} milliseconds timeout.", phase, Task.CurrentId, delay);
        if (delay > 0)
        {
            Console.WriteLine("Do wait for {0} milliseconds.", delay);
            await Task.Delay(delay);
        }

        Console.WriteLine("ending phase {0}", phase);
    }
}

问题似乎在awaiting等待所谓的awaiter任务:

Entering phase 1 (in Task ) with 2000 milliseconds timeout.
Do wait for 2000 milliseconds.
ending phase 1
Entering phase 2 (in Task 769) with 0 milliseconds timeout.
ending phase 2
Entering phase 3 (in Task 770) with 1500 milliseconds timeout.
Do wait for 1500 milliseconds.
"final" completed with result (770, RanToCompletion)
Done.
ending phase 3

这只是不支持吗?我以为我非常了解Task API,但显然我不了解。我认为我可以将其转换为不使用async 或任务,而只是完全同步地执行该方法,但这似乎是一种不好的做事方法。我想要运行的延续并不完全是这个(它们只接受CancellationToken)。任务之间的消息没有特别的依赖关系——我只需要保留一些排序的概念。

谢谢。

编辑:我在上面错误地使用了awaiting这个词:我知道访问Task.Result是完全同步的。我很抱歉。

编辑 2: 我预期会发生的是调用 ContinueWith(_ => worker.Start(2, 0)) 会将任务返回到 ContinueWith,当我的用户委托时,TPL 将在内部等待 worker.StartWork 返回的任务返回了一个任务。查看ContinueWith 的重载列表,这显然是不正确的。我试图解决的部分问题是如何等待安排工作的Main 方法;我不想在所有延续完成之前退出。

我使用ContinueWith 的动机是我有类似以下的要求:

  1. 主要方法分为三个阶段。
  2. 第 1 阶段创建三个工人:abc
  3. 阶段 2 在完成任务 bc 后启动一个额外的工作人员 d,在完成 ab 后启动另一个工作人员 e(依赖项是这些任务必须是按此顺序创建)
  4. 在完成所有工作之前,将遵循与此类似的过程。

如果我正确理解 cmets 中的反馈,我基本上有两种方法可以做到这一点:

  1. 使方法同步,并自己注册延续。
  2. 保留标记为async Task 的方法,并使用await 关键字和Task.WhenAll API 来安排延续。

【问题讨论】:

  • 我认为查看文档的原因是因为我没有通过TaskContinuationOptions.AttachedToParent,所以Task 延续是在分离状态下创建的。有没有更熟悉的人能够偶然证实这一点?
  • 我很困惑为什么你会认为会发生其他事情。您已经创建了一个异步工作流,其中大部分任务都没有等待;正如我一直在说的那样:await 是异步工作流上的排序操作。您几乎没有进行任何排序,因此应该预料到并非所有内容都按顺序运行,对吧?
  • 更清楚地说出您期望在这里发生的事情以及您期望它发生的原因,并且有人可以向您解释您的想法出错的地方。
  • 你需要await你的任务。 ContinueWith 也是老派,现在我们有 await
  • @EricLippert,我很抱歉;我认为这个问题很清楚。我已经更新了问题以试图澄清。感谢您的反馈。

标签: c# multithreading asynchronous


【解决方案1】:

凯文的回答很好。但是基于 cmets,您似乎相信“继续”在描述工作流程的顺序时以某种方式为您提供了比 await 更多的权力。它不是。此外,如何在不诉诸明确延续的情况下正确构建第二次编辑的工作流程的问题尚未得到解决。

让我们看看你的场景。你的工作流程是:我们有任务A、B、C、D和E。开始D取决于完成B和C;开始E取决于A和B的完成。

轻松搞定。请记住:await 是对任务的排序操作。任何时候我们想说“Y 必须在 X 之后”,我们只需在 Y 开始之前的任何地方放置一个 await X。相反,如果我们不希望某个任务在某事之前被强制完成,我们不会等待它。

这是一个很小的框架;这可能不是我编写真实代码的方式,但它清楚地说明了工作流程。

    private async Task DoItAsync(string s, int d)
    {
        Console.WriteLine($"starting {s}");
        await Task.Delay(d * 1000);
        Console.WriteLine($"ending {s}");
    }

    private async Task DoItAsync(Task pre1, Task pre2, string s, int d)
    {
        await pre1;
        await pre2;
        await DoItAsync(s, d);
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        Task atask = DoItAsync("A", 2);
        Task btask = DoItAsync("B", 10);
        Task ctask = DoItAsync("C", 2);
        Task bcdtask = DoItAsync(btask, ctask, "D", 2);
        Task abetask = DoItAsync(btask, atask, "E", 2);
        await bcdtask;
        await abetask;
    }

跟随。 A、B 和 C 启动。 (请记住,它们已经是异步的。“等待”不会使它们异步;等待在工作流中引入了一个排序点。)

接下来我们的两个辅助任务开始了。 D的前置条件是B和C,所以我们await B。假设B不完整,那么await返回一个任务给调用者,代表“b和c完成后启动d,等待d完成”的工作流程”。

现在我们开始第二个辅助任务。再次,它等待 B。让我们再次假设它是不完整的。我们返回调用者。

现在我们将最后一点结构添加到我们的工作流程中。 在两个辅助任务完成之前,工作流并不完整。在 D 和 E 完成之前,这两个辅助任务不会完成,甚至在 B 和 C(在 D 的情况下)或 B 和 A(在 E 的情况下)完成之前,它们都不会开始。

使用这个小框架来处理工作完成的时间,您会发现使用await 在工作流中建立依赖关系非常简单。 这就是它的用途。之所以称为 await,是因为它异步等待任务完成

【讨论】:

  • 感谢您发布此 Eric。由于排序方面的原因,我已将此标记为答案 - 这是我真正想要解决的问题。
  • @PSGuy:很好。请注意,我在这里使用 Windows 窗体作为我的小测试应用程序的基础,因为我发现在具有消息循环和单元线程模型的环境中更容易推理异步。在尝试异步的想法时,我尽量避免使用控制台应用程序。
【解决方案2】:

我预计会发生的是,调用 ContinueWith(_ => worker.Start(2, 0)) 会将任务返回给 ContinueWith,当我的用户委托返回时,TPL 将在内部等待 worker.StartWork 返回的任务一个任务。查看 ContinueWith 的重载列表,这显然是不正确的。

这确实是你错过的。在您的情况下,.ContinueWith 将返回 Task<Task> 而不仅仅是像您预期的那样 Task。也就是说,可以通过使用Unwrap 方法将嵌套任务转换为单个任务来轻松解决此问题:

var worker = new Worker();

var work = worker.StartWork(1, 2000);

var final = work.ContinueWith(_ => worker.StartWork(2, 0)).Unwrap()
    .ContinueWith(ant => worker.StartWork(3, 1500)).Unwrap();

var awaiter = final.ContinueWith(_ => Tuple.Create(_.Id, _.Status));
Console.WriteLine("\"final\" completed with result {0}", awaiter.Result);
Console.WriteLine("Done.");

这将为您提供所需的输出。但正如其他人所提到的,您可能希望改用 async/await,因为它会使您的代码更易于阅读和遵循(并保护您免受ContinueWith 的某些极端情况的影响):

static async Task DoWork()
{
    var worker = new Worker();

    await worker.StartWork(1, 2000);
    await worker.StartWork(2, 0);
    await worker.StartWork(3, 1500);
}

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

【讨论】:

    猜你喜欢
    • 2011-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多