簡體   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