为什么此任务会提前返回?我做错了什么吗?



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

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

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

这只是不支持吗?我以为我非常了解TaskAPI,但显然我并不了解。我认为我可以将其转换为不使用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. 接下来是类似的过程,直到所有工作完成。

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

  1. 使方法同步,并自己注册延续。
  2. 将方法标记为async Task,并使用await关键字和Task.WhenAllAPI 来计划延续。

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

让我们看一下您的方案。您的工作流程是:我们有任务 A、B、C、D 和 E.开始 D 取决于 B 和 C 的完成;开始 E 取决于 A 和 B 的完成。

轻松完成。请记住:等待是对任务的排序操作。 每当我们想说"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 已启动。 (请记住,它们已经是异步的。 "等待"不会使它们异步;await 在工作流中引入了排序点。

接下来,启动两个帮助程序任务。D 的前提条件是 B 和 C,所以我们等待 B。假设 B 未完成,所以 await 向调用方返回一个任务,表示"在 b 和 c 完成后启动 d,等待 d 完成"的工作流。

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

现在,我们将最后一点结构添加到我们的工作流程中。在两个帮助程序任务完成之前,工作流尚未完成。这两个帮助程序任务在 D 和 E 完成之前不会完成,甚至直到 D 和 C 完成,或者 B 和 A(对于 E 完成)才会启动。

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

我期望发生的是调用 ContinueWith(_ => worker。Start(2, 0)) 会将任务返回给 ContinueWith,TPL 将在内部等待 worker 返回的任务。当我的用户委托返回任务时启动工作。查看 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.");
}  

相关内容

最新更新