C#任务汇总

时间:2018-12-05 01:45:47

标签: c# linq async-await fold

比方说,我有许多任务,我想一一延迟执行它们。我的想法是通过与Aggregate组合并在每对之间插入ContinueWithTask.Delay()折叠成一个任务:

var tasks = new[] {1, 2, 3, 4, 5}.Select(async x => Console.WriteLine(x));
var superTask =
    tasks.Aggregate(Task.CompletedTask, async (task1, task2) =>
        await (await task1.ContinueWith(_ => Task.Delay(1000))).ContinueWith(_ => task2));

await superTask;

问题是为什么它不起作用?

2 个答案:

答案 0 :(得分:0)

有几个问题。首先,您不必等待延迟,因此延迟甚至不存在。其次,以这种方式调用Tasks会立即自动启动-您需要改用构造函数方法(请参见this question)。

这部分代码将满足您的需求。

var tasks = new[] { 1, 2, 3, 4, 5 }
    .Select
    (
        x => new Task( () => Console.WriteLine(x) )
    );
var superTask =
    tasks.Aggregate
    (
        Task.CompletedTask,
        async (task1, task2) =>
        {
            await task1;
            await Task.Delay(1000);
            task2.Start();
        }
    );
await superTask;

所有这些,我高度怀疑这是正确的方法。只需编写一个简单的循环来遍历数组,然后一次处理它们。甚至不需要异步。

答案 1 :(得分:0)

您写道:

  

我想一个接一个地执行它们,并且之间要有延迟。

您的代码有几个问题。

在您的示例中,您创建了一个未枚举的枚举。因此,您的任务尚未开始。它们将在您开始枚举时立即启动。因此,一旦您使用foreachToList()或低级枚举:GetEnumerator()MoveNext()

var tasks = new[] {1, 2, 3, 4, 5}.Select(async x => Console.WriteLine(x))
     .ToList();

每个任务一创建就开始运行,因此现在它们同时运行。

您的汇总函数还会枚举每个元素,并执行您的ContinueWith。枚举已经开始执行任务。

看看source code of Enumerable.Aggregate

public static TSource Aggregate<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, TSource, TSource> func)
    {
        ... // some test for null
        using (IEnumerator<TSource> e = source.GetEnumerator())
        {
            if (!e.MoveNext()) throw Error.NoElements(); // there must be at least one element

            TSource result = e.Current;
            while (e.MoveNext()) result = func(result, e.Current);
            return result;
        }
    }

它的作用是什么

  • 第一个MoveNext()执行一次Select语句。第一个任务已创建,并计划尽快(可能立即)运行。
  • e.Current用于保存此运行任务变量Result
  • 第二个MoveNext()再次执行Select语句。创建第二个任务,并计划将其尽快运行,并立即运行。
  • e.Current包含您的第二项任务。它用于执行func(result, e.Current)

此功能将执行以下操作:

<first task>.ContinueWith(delay task).ContinueWith(<2ndTask>).

将结果放入可变结果中,完成MoveNext,等等。

请记住:第一个任务和第二个任务已经在运行!

因此,实际上您的集合是这样的:

Task result = empty task
for every task
{
     Create it and schedule it to start running as soon as possible
     Result = Result.ContinueWith(delayTask).ContinueWith(already running created task)
}

现在,如果您执行一个正在运行的任务并且ContinueWith另一个已经在运行的任务会发生什么?

如果您查看Task.ContinueWith,它表示continuationAction的{​​{1}}参数表示:

  

任务完成时要运行的动作。

如果运行已经运行的任务会怎样?我不知道,一些测试代码将为您提供答案。我最好的猜测是它什么也不会做。当然,它不会停止已经运行的任务。

这不是您想要的!

在我看来,这不是您想要的。您要指定一些操作以在延迟时间之间顺序执行。像这样:

ContinueWith

所以,您想要的是一个函数,该函数输入了一系列的动作和一个DelayTime。每个动作都将开始,等待直到完成,然后等待DelayTime。

您可以使用汇总来执行此操作,其中输入是一系列操作。结果将是可怕的。难以阅读,测试和维护。一个过程将更容易理解。

要使其与LINQ兼容,我将创建一个扩展方法。参见extension methods demystified

Create a Task for Action 1, and wait until finished
Delay
Create a Task for Action 2, and wait until finished
Delay
...

好吧,如果您确实希望使用汇总表来打动您的同事:

static Task PerformWithDelay(this IEnumerable<Action> actionsToPerform, TimeSpan delayTime)
{
    var actionEnumerator = actionsToPerform.GetEnumerator();

    // do nothing if no actions to perform
    if (!actionEnumerator.MoveNext())
        return Task.CompletedTask;
    else
    {    // Current points to the first action
         Task result = actionEnumerator.Current;

         // enumerate over all other actions:
         while (actionEnumerator.MoveNext())
         {
             // there is a next action. ContinueWith delay and next task
             result.ContinueWith(Task.Delay(delayTime)
                   .ContinueWith(Task.Run( () => actionEnumerator.Current);
         } 
     }
}

因此,对于操作序列中的每个操作,我们将序列的第一项作为聚合的种子:ContinueWith一个新的Task.Delay,ContinueWith一个新的执行该操作的Task。

问题:如果您输入的内容为空,则为异常。