纯粹的计算任务顺序执行

时间:2014-09-17 23:14:38

标签: c# parallel-processing async-await

我有几个计算密集型方法,我试图使用async-await并行运行。

我有一个大约80,000个对象的列表,我将这些对象提供给一个返回任务的函数:

public static void Main(string[] args)
{
    //...blah blah blah...

    var runner = new Runner(); //in a nutshell, I manage to get an object that has an async method on it.
    runner.Run().Wait(); //and I wait for it to complete.

    //...blah blah blah...
}

我在我的跑步者对象中有以下方法(或多或少......这是一个人为的例子):

public async Task Run()
{
    var items = ... //this is my list
    var tasks = items.Select(i => this.RunItemAsync(i)).ToArray();

    //I don't get here until the tasks are all finished...every single one...

    await Task.WhenAll(tasks).ConfigureAwait(false);
}

private async Task RunItemAsync(Item i)
{
    var subItems = i.GetSubItems();

    var tasks = subItems.Select(s => s.RunSubItemAsync(s)).ToArray();

    //I don't get here until the sub item tasks are all finished...

    await Task.WhenAll(tasks).ConfigureAwait(false);

    //does computations, doesn't wait on any async i/o, etc
    await this.ProcessAsync(i).ConfigureAwait(false);
}

private async Task RunSubItemAsync(SubItem s)
{
    //does computations, doesn't wait on any async i/o, etc
    ...
}

在过去一年左右的时间里,我一直在努力克服异步等待,有时候会使用TPL Dataflow实现出色的性能和一些非常酷的东西,但每隔一段时间我就会遇到这样的问题;似乎把任务交给"激活"他们的并行能力。这个特殊项目将在具有~16个内核的服务器上运行,所以我真的想利用它。我的开发虚拟机只分配了2个核心,但这仍然允许任务激活并并行运行(过去也是如此)。

我的观察

  • 我设法通过在await Task.Delay(1).ConfigureAwait(false)方法的开头插入一个小RunItemAsync来并行运行。我知道这创造了某种形式的呼吸空间"这允许另一个任务使用该线程。然而,这还不够,因为它很脏,不可靠,并且要求我有不可接受的延迟。
  • 如果没有上述Delay调用,则所有任务都会在Main Thread上运行。这对我来说很明显,因为Main是启动它的功能。我对此没有任何问题,但我过去曾experiencesnew Thread创建的线程上运行任务导致它无法使用默认任务调度程序运行,并且每个任务最终都按顺序运行在那个线程上。也许Main Thread属于这一类?

我的问题

我知道运行ToArray本身不会执行异步代码。但是,我想要发生的事情是,当我的RunItemAsync方法达到第一个await时,它将会停止"并允许调用ToArray的下一次迭代运行。

我也明白添加await Task.Delay是有效的,因为它确实引起了我想要的内容。必须有一些方法可以做到这一点,而无需诉诸await Task.Delay ...

如何并行启动所有这些计算绑定的任务,而不会无意中导致它们按顺序运行?

2 个答案:

答案 0 :(得分:5)

目前有四种主要的并发库/技术可用。

  • async最适合自然异步单一操作,例如I / O.
  • 任务并行库(TPL)最适合并行化CPU绑定工作。
  • TPL Dataflow跨越async并行,为处理数据提供网格/管道抽象。
  • Reactive Extensions(Rx)在概念上类似于TPL Dataflow,但没有并行功能,而是具有大量与时间相关的功能。

在您的情况下,您想要使用TPL。一个简单的Parallel.ForEach就足够了。

最后要注意的是,同步代码(包括CPU绑定的并行代码)应该具有同步API;和异步代码应该有一个异步API。所以你希望你的API看起来是同步的,而不是异步的。

所以,像这样:

public static void Main(string[] args)
{
  var runner = new Runner();
  runner.Run();
}

public void Run()
{
  var items = ...
  Parallel.ForEach(items, i => this.RunItem(i));
}

private void RunItem(Item i)
{
  var subItems = i.GetSubItems();
  Parallel.ForEach(subItems, s => s.RunSubItem(s));
  this.Process(i);
}

private void RunSubItem(SubItem s)
{
  SemaphoreSlim.Wait(); // instead of WaitAsync
  ...
}

答案 1 :(得分:1)

我可以理解你是如何在使用await运行并行任务时遇到的,因为它的目的是在等待的任务完成之前暂停执行该方法" 。如果你真的想要并行做事,await可能不是你想要的。

await的强大功能是允许您按顺序声明将按顺序执行但彼此异步执行的操作,同时确保将操作的结果编组回特定线程(当{{1}时}} 未使用)。您可以使用等待... ConfigureAwait(false),但您实现了目的,并且生成的代码可能会更慢......

根据您发布的内容,您似乎想要并行处理项目的子项目,并按顺序处理项目。例如同时处理第一项的所有子项,一旦完成,同时处理第二项的所有子项,等等。如果这不正确,你的代码并没有真正反映出来。

如果您要执行的操作是并行启动多个任务,请避免在单个任务上使用ConfigureAwait(false),并且只使用单个await个对象。

例如:

Task

但是,很难说你正在尝试用你发布的内容做什么。这似乎过于复杂。如果您想要并行执行大量未定义的任务,那么产生许多 public Task Run() { var items = GetItems(); var tasks = items.Select(RunItemAsync); return Task.WhenAll(tasks); } private Task RunItemAsync(Item i) { var subItems = i.GetSubItems(); var tasks = subItems.Select(s => Task.Factory.StartNew(()=>s.RunSubItem(s))); return Task.WhenAll(tasks).ContinueWith(_ => ProcessAsync(i), TaskContinuationOptions.ExecuteSynchronously); } 并不是最好的方法。你有一个有限数量的CPUS / Cores,如果你有比CPU更多的CPU绑定线程,你真的只是让事情变慢(参见context switch)。您可能需要的是队列任务,这些任务最多可以分批处理 x 任务(其中x是CPU /核心数)。 可以使用Task完成。但是,在任何一种情况下,你都在说一些与你设计的完全不同的东西。

仅仅因为Parallel.ForEach可用,并不意味着您必须将其用于所有线程场景。