管理许多重复的、CPU密集型的任务,并行运行



我需要尽可能快地不断执行20次重复的、CPU密集型的计算。因此,在中有20个任务包含循环方法

while(!token.IsCancellationRequested) 

以尽可能快地重复它们。所有计算都是同时进行的。不幸的是,这使得程序没有响应,所以添加了:

await Task.Delay(15); 

在这一点上,程序并没有挂起,但添加延迟是不正确的方法,它不必要地降低了计算速度。它是没有MVVM的WPF程序。你建议用什么方法让所有20项任务同时工作?一旦完成,它们中的每一个都会不断重复。我希望将CPU(所有内核)的利用率保持在最大值(或接近),以确保最佳效率。

编辑:有20个控件,用户可以在其中调整一些参数。计算在中完成

private async Task Calculate()
{
Task task001 = null;
task001 = Task.Run(async () => 
{
while (!CTSFor_task001.IsCancellationRequested)
{
await Task.Delay(15);
await CPUIntensiveMethod();
}
}, CTSFor_task001.Token);
}

每个控件都是独立的。计算是100%的CPU绑定,没有I/O活动。(所有值都来自变量)在计算过程中,一些UI项目的值发生了变化:

this.Dispatcher.BeginInvoke(new Action(() =>
{
this.lbl_001.Content = "someString";
}));

让我把整件事写成一个答案。你混淆了两个相关但最终独立的概念(谢天谢地,这就是为什么你可以从区别中受益)。请注意,这些都是我对概念的定义——你会听到很多相同事物的不同名称,反之亦然。

异步性是指打破强加的操作的同步性(即操作1等待操作2,操作3等待操作4…)。对我来说,这是一个更普遍的概念,但现在它更常用于表示我所说的"固有异步性",即算法本身是异步的,我们之所以只使用同步编程,是因为我们必须这样做(多亏了awaitasync,我们不再需要了,耶!)。

这里的关键思想是等待。我不能在CPU上做任何事情,因为我正在等待I/O操作的结果。这种异步编程基于这样一种思想,即异步操作几乎没有CPU——它们是I/O绑定的,而不是CPU绑定的。

并行性是一种特殊的一般异步性,在这种异步性中,操作并不是主要等待彼此。换句话说,我不是在等待,我是在工作。如果我有四个CPU核心,我可以理想地使用四个计算线程进行这种处理——在理想的情况下,我的算法将随着可用核心的数量线性扩展。

对于异步性(等待),无论可用逻辑核心的数量如何,使用更多的线程都将提高明显的速度。这是因为99%的时候,代码实际上没有做任何工作,只是在等待。

使用并行性(工作),使用更多线程与可用工作核心的数量直接相关。

线条模糊了很多。这是因为你可能甚至不知道正在发生的事情,例如CPU(以及整个计算机)本身就非常异步——它所显示的明显同步性只是为了让你能够同步地编写代码;所有的优化和异步性都受到这样一个事实的限制,即在输出时,一切都是同步的。如果每次执行i ++时CPU都必须等待来自内存的数据,那么无论您的CPU是在3GHz还是100MHz下运行都无关紧要。你出色的3GHz CPU 99%的时间都会闲置在那里。

也就是说,你的计算任务是CPU绑定的。它们应该使用并行性来执行,因为它们是在工作。另一方面,UI是I/O绑定的,应该使用异步代码。

事实上,async Calculate方法所做的只是掩盖了它实际上是而不是本质异步的事实。相反,您希望将其异步运行到I/O。

换句话说,不是Calculate方法是异步的。是UI希望它与自己异步运行。去掉所有Task.Run的杂物,它不属于那里。

下一步该怎么办?这取决于您的用例。基本上有两种情况:

您希望任务始终运行,始终在后台,从头至尾。在这种情况下,只需为它们中的每一个创建一个线程,并且根本不使用Task。您可能还想探索一些选项,如生产者-消费者队列等,以优化不同可能的计算任务的实际运行时间。实际的实现与您实际处理的内容紧密相连。

或者,您希望在UI操作上启动任务,然后在结果准备就绪时将结果值返回到启动它们的UI方法中。在这种情况下,await终于发挥作用:

private btn_Click(object sender, EventArgs e)
{
var result = await Task.Run(Calculate);
// Do some (little) work with the result once we get it
tbxResult.Text = result;
}

async关键字实际上在代码中根本没有位置。

希望现在这个问题更清楚了,可以问更多的问题。

因此,您真正想要的是澄清一种在保持UI响应的同时最大限度地提高性能的良好做法。正如Luaan所澄清的,你的提案中的asyncawait部分对你的问题没有好处,Task.Run不适合你的工作;使用线程是一种更好的方法。

定义一个线程数组,以便在每个逻辑处理器上运行一个线程。在它们之间分配任务数据,并通过TPL DataFlow library中提供的BufferBlock控制20次重复计算。

为了保持UI的响应,我建议两种方法:

  1. 您的计算需要多次频繁的UI更新:将所需的更新信息放入队列中,并在Timer事件中更新
  2. 您的计算需要很少的UI更新:使用类似Control.BeginInvoke的调用方法更新UI

正如@Luaan所说,我强烈建议阅读async/await,关键是它没有引入任何并行性。

认为您正在尝试做的事情类似于下面的简单示例,在该示例中,您在线程池上启动CPUIntensiveMethod并等待其完成。awaitCalculate方法返回控制(允许UI线程继续工作),直到任务完成,此时它继续while循环。

private async Task Calculate()
{
while (!CTSFor_task001.IsCancellationRequested)
{
await Task.Run(CPUIntensiveMethod);
}   
}

最新更新