Task.WhenAll(IEnumerable):任务启动两次?

时间:2017-08-25 08:22:21

标签: c# async-await

我偶然发现Task.WhenAll的一个重载,一个以IEnumerable为参数的重载

public static Task WhenAll(IEnumerable<Task<TResult>> tasks)

我以为我会用以下短程序尝试这个功能。

在测试课程中:

// contains the task numbers that has been run
private HashSet<int> completedTasks = new HashSet<int>();

// async function. waits a while and marks that it has been run:
async Task<int> Calculate(int taskNr)
{
     string msg = completedTasks.Contains(taskNr) ?
         "This task has been run before" :
         "This is the first time this task runs";
    Console.WriteLine($"Start task {i} {msg}");

    await Task.Delay(TimeSpan.FromMilliseconds(100));

    Console.WriteLine($"Finished task {taskNr}");
    // mark that this task has been run:
    completedTasks.Add(taskNr);
    return i;
}

// async test function that uses Task.WhenAll(IEnumerable)
public async Task TestAsync()
{
    Console.Write("Create the task enumerators... ");
    IEnumerable<Task<int>> tasks = Enumerable.Range(1, 3)
        .Select(i => Calculate(i));
    Console.WriteLine("Done!");

    Console.WriteLine("Start Tasks and await");
    await Task.WhenAll(tasks);
    Console.WriteLine("Finished waiting. Results:");

    foreach (var task in tasks)
    {
        Console.WriteLine(task.Result);
    }
}

最后主程序:

static void Main(string[] args)
{
     var testClass = new TestClass();
     Task t = Task.Run(() => testClass.TestAsync());
     t.Wait();
}

输出如下:

Create the task enumerators... Done!
Start Tasks and wait
Start task 1 This is the first time this task runs
Start task 2 This is the first time this task runs
Start task 3 This is the first time this task runs
Finished task 2
Finished task 3
Finished task 1
Finished waiting. Results:
Start task 1 This task has been run before
Finished task 1
1
Start task 2 This task has been run before
Finished task 2
2
Start task 3 This task has been run before
Finished task 3
3

显然每项任务都会运行两次!我做错了什么?

更奇怪的是:如果我在Task.Whenall之前使用ToList()枚举任务序列,则该函数按预期工作!

1 个答案:

答案 0 :(得分:9)

您的问题是延迟执行。改变这一行

IEnumerable<Task<int>> tasks = Enumerable.Range(1, 3)
    .Select(i => Calculate(i));

var tasks = Enumerable.Range(1, 3)
    .Select(i => Calculate(i)).ToList();

Select() 不执行&#34;查询&#34;立即,但返回一个普查员。只有当您使用此枚举器迭代任务时,才会为序列1 ... 3调用内部lambda 在您的版本中,每次遍历tasks时,都会再次调用Calculate(i)并创建新任务。
使用.ToList()枚举器执行一次,结果序列Task<int>存储在List<Task<int>>中(当第二次枚举该列表时不再生成)。

当您致电Task.WhenAll(tasks)时,此方法会迭代tasks,从而启动每项任务。当您稍后再次迭代时(使用foreach循环输出结果),查询将再次执行,从而启动新任务。