[英]Multiple parallel Tasks in C# do not improve calculation time
我有一个复杂的数学问题要解决,我决定并行进行一些独立计算以缩短计算时间。 在许多 CAE 程序中,如 ANSYS 或 SolidWorks,可以为此目的设置多个核心。
我创建了一个简单的 Windows 窗体示例来说明我的问题。 这里的功能CalculateStuff()
提出了A
从Sample
类功率1.2
max
倍。 对于 2 个任务, max / 2
次,对于 4 个任务, max / 4
次。
我仅计算了一个CalculateStuff()
函数或四个重复项( CalculateStuff1(), ...2(), ...3(), ...4()
- 每个任务一个)的结果操作时间相同的代码。 我不确定,是否对每个任务使用相同的函数很重要(无论如何, Math.Pow
是相同的)。 我还尝试启用或禁用 ProgressBar。
该表表示所有 12 种情况的操作时间(秒)。 我预计 2 和 4 个任务的速度会快 2 倍和 4 倍,但在某些情况下,4 个任务甚至比 1 个任务还要糟糕。我的计算机有 2 个处理器,每个有 10 个内核。 根据调试窗口,CPU 使用率随着任务的增加而增加。 我的代码有什么问题,或者我误解了什么? 为什么多个任务不能提高操作时间?
private readonly ulong max = 400000000ul;
// Sample class
private class Sample
{
public double A { get; set; } = 1.0;
}
// Clear WinForm elements
private void Clear()
{
PBar1.Value = PBar2.Value = PBar3.Value = PBar4.Value = 0;
TextBox.Text = "";
}
// Button that launches 1 task
private async void BThr1_Click(object sender, EventArgs e)
{
Clear();
DateTime start = DateTime.Now;
Sample sample = new Sample();
await Task.Delay(100);
Task t = Task.Run(() => CalculateStuff(sample, PBar1, max));
await t;
TextBox.Text = (DateTime.Now - start).ToString(@"hh\:mm\:ss");
t.Dispose();
}
// Button that launches 2 tasks
private async void BThr2_Click(object sender, EventArgs e)
{
Clear();
DateTime start = DateTime.Now;
Sample sample1 = new Sample();
Sample sample2 = new Sample();
await Task.Delay(100);
Task t1 = Task.Run(() => CalculateStuff(sample1, PBar1, max / 2));
Task t2 = Task.Run(() => CalculateStuff(sample2, PBar2, max / 2));
await t1; await t2;
TextBox.Text = (DateTime.Now - start).ToString(@"hh\:mm\:ss");
t1.Dispose(); t2.Dispose();
}
// Button that launches 4 tasks
private async void BThr4_Click(object sender, EventArgs e)
{
Clear();
DateTime start = DateTime.Now;
Sample sample1 = new Sample();
Sample sample2 = new Sample();
Sample sample3 = new Sample();
Sample sample4 = new Sample();
await Task.Delay(100);
Task t1 = Task.Run(() => CalculateStuff(sample1, PBar1, max / 4));
Task t2 = Task.Run(() => CalculateStuff(sample2, PBar2, max / 4));
Task t3 = Task.Run(() => CalculateStuff(sample3, PBar3, max / 4));
Task t4 = Task.Run(() => CalculateStuff(sample4, PBar4, max / 4));
await t1; await t2; await t3; await t4;
TextBox.Text = (DateTime.Now - start).ToString(@"hh\:mm\:ss");
t1.Dispose(); t2.Dispose(); t3.Dispose(); t4.Dispose();
}
// Calculate some math stuff
private static void CalculateStuff(Sample s, ProgressBar pb, ulong max)
{
ulong c = max / (ulong)pb.Maximum;
for (ulong i = 1; i <= max; i++)
{
s.A = Math.Pow(s.A, 1.2);
if (i % c == 0)
pb.Invoke(new Action(() => pb.Value = (int)(i / c)));
}
}
任务不是线程。 “异步”并不意味着“同时”。
我的代码有什么问题,或者我误解了什么?
你误解了什么是任务。
您应该将任务视为可以按您想要的任何顺序执行的任务。 以烹饪食谱为例:
如果这些不是任务而是同步代码,您将始终按照它们列出的确切顺序执行这些步骤。
如果它们是任务,那并不意味着这些工作将同时完成。 你只是一个人(=一个线程),你一次只能做一件事。
您可以按您喜欢的任何顺序执行任务,您甚至可以暂停一项任务以开始另一项任务,但您仍然不能同时做多于一件事。 无论您完成任务的顺序如何,完成所有三个任务所花费的总时间保持不变,并且这(本质上)并没有更快。
如果它们是线程,那就是雇佣 3 个厨师,这意味着这些工作可以同时完成。
异步性确实减少了等待的空闲时间。
请注意,异步代码可能会在同步代码空闲的情况下导致时间增加,例如等待网络响应。 上面的例子中没有考虑到这一点,这正是我列出“cut [x]”作业而不是“wait for [x] to bool”的原因。
您的工作(计算)不是异步代码。 它从不空闲(以一种可等待的方式),因此它同步运行。 这意味着您不会从异步运行中获得任何好处。
将您的代码简化为一个更简单的示例:
private static void CalculateStuff(Sample s, ProgressBar pb, ulong max)
{
Thread.Sleep(5000);
}
很简单地说,这项工作需要 5 秒,不能等待。 如果您同时运行其中 3 个任务,它们仍将一个接一个处理,总共需要15 秒。
如果您的任务中的工作实际上是可等待的,您会看到时间上的好处。 例如:
private static async void CalculateStuff(Sample s, ProgressBar pb, ulong max)
{
await Task.Delay(5000);
}
这项工作需要 5 秒,但可以等待。 如果您同时运行其中 3 个任务,您的线程将不会浪费时间空闲(即等待延迟),而是会开始执行以下任务。 由于它可以同时等待(即什么都不做)这些任务,这意味着总处理时间总共需要5 秒(加上一些可以忽略不计的开销成本)。
根据调试窗口,CPU 使用率随着任务的增加而增加。
任务管理的开销很小,这意味着与同步代码相比,工作总量(可以用 CPU 使用率随时间推移来衡量)略高。 这是可以预料的。
与从编写良好的异步代码中获得的好处相比,这种小成本通常相形见绌。 但是,您的代码根本没有利用异步性带来的实际好处,因此您只看到了开销成本,而没有看到它的好处,这就是为什么您的监控会为您提供与预期相反的结果。
- 我的电脑有 2 个处理器,每个处理器 10 个内核。
CPU 内核、线程和任务是三种截然不同的野兽。
任务由线程处理,但它们不一定具有一对一的映射关系。 以 4 个开发人员的团队为例,该团队有 10 个需要解决的错误。 虽然这意味着不可能同时解决所有 10 个错误,但这些开发人员(线程)可以一个接一个地接下工单(任务),每当他们完成上一个工单时就接下新的工单(任务)(任务)。
CPU 内核就像工作站。 工作站(CPU 内核)比开发人员(线程)少是没有意义的,因为您最终会遇到闲置的开发人员。
此外,您可能不希望您的开发人员能够声明所有工作站。 也许 HR 和会计(= 其他操作系统进程)也需要有一些有保障的工作站,以便他们可以完成他们的工作。
公司(= 计算机)不会因为开发人员正在修复一些错误而陷入停顿。 这是过去在单核机器上发生的情况——如果一个进程声明了 CPU,其他事情就不会发生。 如果该进程需要很长时间或挂起,则一切都会冻结。
这就是为什么我们有一个线程池。 这里没有直接的现实世界类比(除非可能是咨询公司动态调整向贵公司派遣多少开发人员),但线程池基本上能够决定允许在该公司工作的开发人员数量。同时为了确保开发任务能尽快被看到,同时也确保其他部门仍然可以在工作站上做他们的工作。
这是一个谨慎的平衡行为,不要派太多的开发人员,因为这会淹没系统,同时也不要派的开发人员太少,因为这意味着工作完成得太慢。
您的线程池的确切配置不是我可以通过简单的问答来解决的问题。 但是,与您拥有的任务数量相比,您描述的行为与具有更少的 CPU(专用于您的运行时)和/或线程是一致的。
有很多可能的原因可能导致您看不到预期的性能提升,包括您的机器内核目前还用于其他用途。 运行这个精简版的代码,我可以在并行运行时看到显着的改进:
private IEnumerable<Sample> CalculateMany(int n)
{
return Enumerable.Range(0, n)
.AsParallel() // comment this to remove parallelism
.Select(i => { var s = new Sample(); CalculateStuff(s, max / (ulong)n); return s; })
.ToList();
}
// Calculate some math stuff
private static void CalculateStuff(Sample s, ulong max)
{
for (ulong i = 1; i <= max; i++)
{
s.A = Math.Pow(s.A, 1.2);
}
}
这是运行CalculateMany,其中n
值为1、2 和4:
我使用Task.Run()
看到类似的结果:
private IEnumerable<Sample> CalculateMany(int n)
{
var tasks =
Enumerable.Range(0, n)
.Select(i => Task.Run(() => { var s = new Sample(); CalculateStuff(s, max / (ulong)n); return s; }))
.ToArray() ;
Task.WaitAll(tasks);
return tasks
.Select(t => t.Result)
.ToList();
}
不幸的是,除了可能正在发生的状态机魔术之外,我无法给您其他原因,但这显着提高了性能:
private async void BThr4_Click(object sender, EventArgs e)
{
Clear();
DateTime start = DateTime.Now;
await Task.Delay(100);
Task<Sample> t1 = Task<Sample>.Run(() => CalculateStuff(PBar1, max / 4));
Task<Sample> t2 = Task<Sample>.Run(() => CalculateStuff(PBar2, max / 4));
Task<Sample> t3 = Task<Sample>.Run(() => CalculateStuff(PBar3, max / 4));
Task<Sample> t4 = Task<Sample>.Run(() => CalculateStuff(PBar4, max / 4));
Sample sample1 = await t1;
Sample sample2 = await t2;
Sample sample3 = await t3;
Sample sample4 = await t4;
TextBox.Text = (DateTime.Now - start).ToString(@"hh\:mm\:ss");
t1.Dispose(); t2.Dispose(); t3.Dispose(); t4.Dispose();
}
// Calculate some math stuff
private static Sample CalculateStuff(ProgressBar pb, ulong max)
{
Sample s = new Sample();
ulong c = max / (ulong)pb.Maximum;
for (ulong i = 1; i <= max; i++)
{
s.A = Math.Pow(s.A, 1.2);
if (i % c == 0)
pb.Invoke(new Action(() => pb.Value = (int)(i / c)));
}
return s;
}
这样您就不会在调用函数中保留任务必须访问的 Sample 实例,而是在任务中创建实例,然后在任务完成后将它们返回给调用者。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.