为什么这个任务早点返回?我做错了什么?

时间:2017-12-02 01:29:40

标签: c# multithreading asynchronous

我试图建立一群具有最小耦合的工人,但我想使用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);
    }
}

问题似乎在等待等待所谓的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会在内部等待{{1}返回的任务当我的用户委托返回一个任务时。查看worker.StartWork的重载列表,这显然是不正确的。我试图解决的部分原因是如何等待安排工作的ContinueWith方法;在所有延续完成之前,我不想退出。

我使用Main的动机是我的要求类似于以下内容:

  1. 主要方法分为三个阶段。
  2. 第1阶段创建了三个工作人员:ContinueWithab
  3. 第2阶段在完成任务cd后启动其他工作人员b,并在c和{{完成后启动另一名工作人员e 1}}(依赖关系是必须按此顺序创建这些任务)
  4. 在完成所有工作之前,将执行与此类似的过程。
  5. 如果我正确理解了评论中的反馈,我基本上有两种方法可以做到这一点:

    1. 使方法同步,并自行注册continuation。
    2. 保留标记为a的方法,并使用b关键字和async Task API来安排续约。

2 个答案:

答案 0 :(得分:7)

凯文的回答很好。但根据评论,你似乎相信"继续"在描述工作流程的顺序时,某种程度上为您提供了比等待更多的能力。它不是。此外,如何在不诉诸显式延续的情况下正确构建第二次编辑的工作流程的问题尚未得到解决。

让我们来看看你的情景。您的工作流程是:我们有任务A,B,C,D和E.起始D取决于B和C的完成;起始E取决于A和B的完成。

轻松完成。请记住: await是对任务的排序操作。任何时候我们都想说" Y必须在X"之后,我们只需在Y启动之前在任何地方等待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,所以我们等待B.假设B是不完整的,所以await将一个任务返回给调用者,表示"在b和c完成后开始d的工作流程,等待d完成"。

现在我们开始第二个辅助任务。它再次等待B.让我们再次假设它不完整。我们回到来电者那里。

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

使用这个小框架来解决完成任务的时间,您将看到使用await在工作流中构建依赖关系非常简单。 那是什么。它被称为await,因为它异步等待任务完成

答案 1 :(得分: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.");
}