繁体   English   中英

C# 中的多个并行任务不会提高计算时间

[英]Multiple parallel Tasks in C# do not improve calculation time

我有一个复杂的数学问题要解决,我决定并行进行一些独立计算以缩短计算时间。 在许多 CAE 程序中,如 ANSYS 或 SolidWorks,可以为此目的设置多个核心。

我创建了一个简单的 Windows 窗体示例来说明我的问题。 这里的功能CalculateStuff()提出了ASample类功率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 使用率随时间推移来衡量)略高。 这是可以预料的。

与从编写良好的异步代码中获得的好处相比,这种小成本通常相形见绌。 但是,您的代码根本没有利用异步性带来的实际好处,因此您只看到了开销成本,而没有看到它的好处,这就是为什么您的监控会为您提供与预期相反的结果。


  1. 我的电脑有 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM