[英]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.