创建多个任务,不要等待它们,而是按顺序 运行

Create multiple tasks, don't await them, but have them run sequentially

我正在尝试一个接一个地执行 2 个任务 运行,但我不想等待它们完成,因为我稍后会这样做。 最初我有一个 ContinueWith 的实现,但是双 await 困扰着我,我不认为 Unwrap() 是一个显着的改进。

我决定做一个 PoC 来分别创建这 2 个任务,并在我 return 2 个初始任务时分派另一个任务来管理它们,但奇怪的是,一旦我点击await Task.Delay() 这基本上意味着实际上它们是 运行 并发的。 这是代码:

var task1 = new Task(async ()=>
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
});
var task2 = new Task(async () => 
{
    Console.WriteLine("Task2 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task2 STOP");
});
var taskParent = Task.Run(async () => 
{
    Console.WriteLine("starting 1");
    task1.Start();
    await task1;
    Console.WriteLine("starting 2");
    task2.Start();
    await task2;
});

Console.WriteLine("BEGIN await parent");
await taskParent;
Console.WriteLine("END await parent");

输出为

BEGIN await parent
starting 1
Task1 Start
starting 2
Task2 Start
END await parent
Task2 STOP
Task1 STOP

所以我希望 task2 在 task1 之后开始,然后在 task1 之前完成。我看不出调用 await Task.Delay 会将任务标记为完成的原因。我错过了什么吗?

编辑 为了简化我的要求,因为似乎有些混乱。 任务必须在等待之前 returned,第二个任务必须在第一个任务之后 运行。 其他一些线程稍后会想要结果,如果完成则很好,否则它将等待。 我需要能够等待第二个任务,并期望第一个任务在第二个任务之前执行。

基本上在 c# 中创建的任务总是 运行,因此如果您创建任务而不等待它,那么它很可能会在单独的线程中 运行。看看:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

要归档顺序执行,您要么必须为它们做一种调度程序,要么在某种锁定机制上实现顺序。

继续是保证这一点的最简单方法。

鉴于所有任务构造函数都接受 Action 或其变体,因此执行

var task1 = new Task(async ()=>
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
});

task1.Start();
await task1;

与做

没有什么不同
void EntryPoint()
{
    CallAsync();
}

async void CallAsync()
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
}

上面的 CallAsync 将 运行 直到第一次挂起 (await Task.Delay(5000)) 之后它注册一个 Timer 回调和 returns 给调用者立即 returns,完全不知道任何 async 语义。

如果应用程序在 Task.Delay 下的 Timer 触发时仍然存在,它将 运行 基于当前 TaskSchedulerSynchronizationContext 像往常一样,将 Task1 STOP 写入控制台。

如果您需要按顺序 运行 任务而不等待它们,请使用 ContinueWith 或实施自定义 TaskScheduler 来为您执行此操作。

或者甚至更好,正如@Panagiotis Kanavos 所建议的那样,创建一个 stand-alone 异步方法,该方法 运行 是任务序列并等待其结果:

async Task PerformThingsAsync()
{
    var task = RunMyTasksAsync();

    // Do things

    await task;
}

async Task RunTasksAsync()
{
    await RunFistTaskAsync();
    await RunSecondTaskAsync();
    // ...
}

任务不是线程。从来没有充分的理由通过其构造函数创建任务并稍后尝试“启动”它。一个任务是一个承诺,某些事情将在未来完成,甚至可能无法执行。例如,Task.Delay 使用计时器向 TaskCompletionSource 发出信号。

无法通过 Start 控制执行,同样是因为任务不是线程。 Start 仅安排任务执行,实际上 运行 并没有。无法保证任务 运行 会按照计划的顺序进行。

await 也不执行任务,它 等待 一个已经激活的任务完成,不会阻塞调用线程。您无需等待任务即可执行。你只需要在你想得到它的结果时等待它,或者等待它完成。

至于问题本身,不清楚是什么问题。如果问题是如何在不等待整个序列的情况下按顺序执行一些异步函数,最简单的方法是将它们放在自己的异步方法中,将其任务存储在变量中并在需要时等待它:

async Task GetAnImageAsync()
{
    //Load a URL from DB using Dapper
    var url=await connection.QueryFirstOrDefault<string>("select top 1 URLs from Pictures");
    //Get the image
    var image=await client.GetByteArrayAsync(url);
    //Save it
    await File.WriteAllBytesAsync("blah.jpg",image);
}

...

async Task DoSomethingElse()
{
    var imageTask=GetAnImageAsync();
    //Do some other work
    ...
    //Only await at the end
    await imageTask();
}

您会经常听到不推荐使用 Task 构造函数,这是一个明智的建议。创建 Func<Task>, and invoke it when you want to start the task, and it's also safer. The Task constructor has the same hidden gotchas with the ContinueWith 方法更容易:它不理解异步委托,并且需要在启动时明确指定调度程序。但是,如果您明确知道 Task 构造函数是解决您的问题的最佳工具,那么您可以使用它:

Task<Task> taskTask1 = new Task<Task>(async () =>
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
});

Task<Task> taskTask2 = new Task<Task>(async () =>
{
    Console.WriteLine("Task2 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task2 STOP");
});

Task taskParent = Task.Run(async () =>
{
    Console.WriteLine("starting 1");
    taskTask1.Start(TaskScheduler.Default);
    await taskTask1.Unwrap();
    Console.WriteLine("starting 2");
    taskTask2.Start(TaskScheduler.Default);
    await taskTask2.Unwrap();
});

Console.WriteLine("BEGIN await parent");
await taskParent;
Console.WriteLine("END await parent");

输出:

BEGIN await parent
starting 1
Task1 Start
Task1 STOP
starting 2
Task2 Start
Task2 STOP
END await parent

请注意,您不需要 Unwrap Task.Run,因为此方法 understands async delegates 会自动为您解包。

还要注意 TaskScheduler.Default 作为参数传递给 Start 方法。不指定调度程序会使您的代码依赖于环境 TaskScheduler.Current,并且可能会在存在 CA2008 分析器的情况下生成警告。