【问题标题】:Using Task.WhenAll with a growing list of Tasks将 Task.WhenAll 与不断增长的任务列表一起使用
【发布时间】:2018-05-21 17:26:55
【问题描述】:

Task.WhenAll(IEnumerable<Task>) 等待 IEnumerable 中的所有任务都完成 --- 但只有第一次调用时列表中的任务。如果有任何活动任务添加到列表中,则不会考虑它们。这个简短的例子展示了:

    List<Task> _tasks = new List<Task>();

    public async Task  QuickExample()
    {
        for(int n =0; n < 6; ++n)
            _tasks.Add(Func1(n));

        await Task.WhenAll(_tasks);     
        Console.WriteLine("Some Tasks complete");

        await Task.WhenAll(_tasks);
        Console.WriteLine("All Tasks complete");
    }


    async Task Func1(int n)
    {
        Console.WriteLine($"Func1-{n} started");
        await Task.Delay(2000);
        if ((n % 3) == 1)
            _tasks.Add(Func2(n));
        Console.WriteLine($"Func1-{n} complete");
    }

    async Task Func2(int n)
    {
        Console.WriteLine($"Func2-{n} started");
        await Task.Delay(2000);
        Console.WriteLine($"Func2-{n} complete");
    }

这个输出:

Func1-0 started
Func1-1 started
Func1-2 started
Func1-3 started
Func1-4 started
Func1-5 started
Func1-5 complete
Func1-3 complete
Func2-1 started
Func1-1 complete
Func1-0 complete
Func1-2 complete
Func2-4 started
Func1-4 complete
Some Tasks complete
Func2-4 complete
Func2-1 complete
All Tasks complete
Done

第二个Task.WhenAll() 解决了这种情况下的问题,但这是一个相当脆弱的解决方案。在一般情况下,处理此问题的最佳方法是什么?

【问题讨论】:

  • 假设另一个线程将任务添加到_tasks。 Task.WhenAll 应该如何知道添加完成?
  • 您可以尝试使您调用的方法更具“功能性”。该方法可以返回它在内部产生的任务集合。这样,您就可以保持代码干净且易于推理。恕我直言,其他可能的解决方案是“hacky”。​​
  • 您只需要WaitAll 已知任务列表。如果您想添加它并继续等待,那么我强烈建议您采用不同的方法。这在许多方面都是有害的,尤其是如果您不是唯一的开发人员。如果你有一个线程添加任务然后等待那个线程然后WaitAll 其余的当它完成添加它们时。只是一个建议而不是解决方案。
  • 您可以简单地从生成它们的任务中await 新任务,而无需将它们级联到_tasks 集合。如果 A 创建 B,则 A 在 B 完成之前不会完成。
  • 您是否考虑过为此使用 Microsoft 的响应式框架?它更适合这种事情。

标签: c# .net task task-parallel-library


【解决方案1】:

您正在修改List&lt;&gt; 而不锁定它...您喜欢过着危险的生活:-) 在执行WaitAll 之前保存CountWaitAll,然后在WaitAll 检查之后_tasksCount。如果不同,请再做一轮(所以你需要在WaitAll 周围加上while

int count = _tasks.Count;

while (true)
{
    await Task.WhenAll(_tasks);

    lock (_tasks)
    {
        if (count == _tasks.Count)
        {
            Console.WriteLine("All Tasks complete");
            break;
        }

        count = _tasks.Count;
        Console.WriteLine("Some Tasks complete");
    }
}

async Task Func1(int n)
{
    Console.WriteLine($"Func1-{n} started");
    await Task.Delay(2000);

    if ((n % 3) == 1)
    {
        lock (_tasks)
        {
            _tasks.Add(Func2(n));
        }
    }

    Console.WriteLine($"Func1-{n} complete");
}

我将添加第二个(可能更正确的解决方案),这与您正在做的不同:您可以简单地 await 来自生成它们的 Tasks 中的新 Tasks,而无需级联它们到_tasks 集合。如果 A 创建 B,则 A 在 B 完成之前不会完成。显然,您不需要将新的 Tasks 添加到 _tasks 集合中。

【讨论】:

  • 嗯,作者为什么要同步访问列表?作者没有同时做任何事情。
  • @Sergey.quixoticaxis.Ivanov 他在async Task Func1(int n) 里面做一个_tasks.Add(Func2(n));。无法保证将用于运行 Task.WhenAll 的线程数以及要执行的多个 Task Func1()
  • async 是异步的,不是并行的。
  • @Sergey.quixoticaxis.Ivanov async 是异步的,但它可能会部分并行,具体取决于任务的编写方式...参见例如here 因此它可以运行为少至 2 个线程或多至 5 个
  • xanatos,当然,它可以在内部产生一千个线程,但它与异步性无关。顺便说一句,如果我没记错的话,Stephen Cleary 有一篇很棒的帖子。正如我在我的 cmets 中对原始帖子所写的那样,恕我直言,函数式风格将解决 OP 的问题,并且在单线程或多线程代码中都能很好地运行。
【解决方案2】:

异步函数将在第一个await返回给调用者。
因此,for 循环将在您将额外任务添加到原始任务列表之前完成。

Task.WhenAll 的实现会将任务迭代/复制到本地列表,因此在调用Task.WhenAll 之后添加的任务将被忽略。

在您的特定情况下,在 await Task.Delay() 之前将呼叫转移到 Func1 可能是一个解决方案。

async Task Func1(int n)
{
    Console.WriteLine($"Func1-{n} started");
    if ((n % 3) == 1)
        _tasks.Add(Func2(n));

    await Task.Delay(2000);
    Console.WriteLine($"Func1-{n} complete");
}

但是如果在实际场景中调用Func2 依赖于一些异步方法的结果,那么你需要一些其他的解决方案。

【讨论】:

    【解决方案3】:

    考虑一下;听起来工作正在从另一个线程提交到“任务列表”。从某种意义上说,“任务提交”线程本身可能是另一个等待你的任务。

    如果您等待所有任务提交,那么您保证您对WhenAll 的下一次调用将产生一个完全完成的有效负载。

    您的等待功能可能/应该是一个两步过程:

    1. 等待“任务提交”任务完成,表示所有任务都已提交
    2. 等待所有提交的任务完成。

    例子:

    public async Task WaitForAllSubmittedTasks()
    {
        // Work is being submitted in a background thread;
        // Wrap that thread in a Task, and wait for it to complete.
        var workScheduler = GetWorkScheduler();
        await workScheduler;
    
        // All tasks submitted!
    
        // Now we get the completed list of all submitted tasks.
        // It's important to note: the submitted tasks
        // have been chugging along all this time.
        // By the time we get here, there are probably a number of
        // completed tasks already.  It does not delay the speed
        // or execution of our work items if we grab the List
        // after some of the work has been completed.
        //
        // It's entirely possible that - by the time we call
        // this function and wait on it - almost all the 
        // tasks have already been completed!
        var submittedWork = GetAllSubmittedTasks();
        await Task.WhenAll(submittedWork);
    
        // Work complete!
    }
    

    【讨论】:

      【解决方案4】:

      由于似乎可以在执行原始任务列表的过程中创建其他任务,因此您将需要一个简单的while 构造。

      while (_tasks.Any( t => !t.IsCompleted ) 
      {
          await Task.WhenAll(_tasks);
      }
      

      这将检查列表中是否有任何未完成的任务并等待它们,直到它在没有任务剩余的时刻捕获列表。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-22
        • 1970-01-01
        • 2012-09-16
        相关资源
        最近更新 更多