並行程式設計和任務(一)
前言
併發、並行。同步、非同步、互斥、多執行緒。我太難了。被這些詞搞懵了。前面我們在寫.Net基礎系列的時候寫過了關於.Net的非同步程式設計。那麼其他的都是些什麼東西呀。今天我們首先就來解決這個問題。把這些詞搞懂搞透。理清邏輯。然後最後我們進入並行程式設計的介紹。
概念初識
首先我們看併發和並行:
併發:併發指的是在作業系統中,一個是時間段內有多個程式在執行,但是呢。這幾個程式都執行在同一個處理機上,並且任意時間點都是一個程式執行在處理機上。
並行:並行指的是在作業系統中,一個時間段內有多個程式在執行,但是呢。這幾個程式分別執行在不同的處理機上。也就是說這些程式是一起執行的。
簡單理解也就是併發就像三個包子給一個人吃,一口吃一個包子。並行就是三個包子給三個人吃,三個人一口分別吃三個包子。
然後我們看看非同步與多執行緒概念:
剛剛我們講到併發的理解概念,其中併發包含兩種關係-同步和互斥。同步和互斥我們都是相對於臨界資源來談的。
互斥:程式間相互排斥使用臨界資源的現象就叫互斥。就好比程式A在訪問List集合的時候,程式B也想訪問,但是A在訪問。B就阻塞等待A訪問完成之後才去訪問。
同步:程式間的關係不是臨界資源的相互排斥,而是相互依賴。例如程式B需要讀取一個集合結果,但是這個集合結果需要程式A返回,當程式A沒有返回集合結果時,程式B就會因為沒有獲得資訊而阻塞。當程式A返回資訊。程式B就可以獲得資訊被喚起繼續執行。
多執行緒:多執行緒可以說是程式設計的一個邏輯概念,多執行緒實現了執行緒的切換。使其看起來似乎是在同時執行多個執行緒一樣。是程式中併發執行的一段程式碼。
非同步:非同步與同步相對應。同步是程式間相互依賴。非同步是程式間相互獨立。不需要等待上一個程式的結果。可以做自己的事情。
上面我們就介紹完了併發、並行、互斥、同步、多執行緒、非同步。我們總結下其中關聯吧:
非同步與多執行緒並不相等。非同步是需要達到的目的,多執行緒是一個是實現非同步的一種手段。最後達到的目的是什麼呢?就是併發中執行緒的切換。同步也可以實現執行緒切換,但是由於同步中IO等待會浪費時間,所以同步切換程式與非同步切換進行就有明顯的時間差距。
Parallel
今天我們介紹的是Parallel類。該類位於System.Threading.Tasks名稱空間中。依次來實現資料和任務的並行性。
其中定義了並行的for和foreach的靜態方法、還包含著Parallel.Invoke()用於任務的並行性。我們下面就來看看吧。
Parallel.For()
Parallel.For()方法類似於#中的for迴圈語句,但是Parallel.For()是可以並行執行的。不過並行執行並不保證迭代執行的順序。我們來看看。
public static void ForEx()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
List<Test> list = new List<Test>();
for (int i = 0; i < 100; i++)
{
Test test = new Test();
test.Name = "Name:" + i;
test.Index = "Index:" + i;
test.Time = DateTime.Now;
list.Add(test);
Task.Delay(10).Wait();
Console.WriteLine("C#中的for迴圈:" + i);
}
stopwatch.Stop();
Console.WriteLine("for 0-100 執行完成 耗時:{0}",stopwatch.ElapsedMilliseconds);
}
public static void ParallelFor()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
List<Test> lists = new List<Test>();
Parallel.For(1,100,i =>
{
Test tests = new Test();
tests.Name = "Name:" + i;
tests.Index = "Index:" + i;
tests.Time = DateTime.Now;
lists.Add(tests);
Task.Delay(10).Wait();
Console.WriteLine("Parallel中的ParallelFor迴圈:" + i);
});
stopwatch.Stop();
Console.WriteLine("ParallelFor 0-100 執行完成 耗時:{0}",stopwatch.ElapsedMilliseconds);
}複製程式碼
static void Main(string[] args)
{
Console.WriteLine("Start");
ForEx();
Console.WriteLine("for迴圈完成");
ParallelFor();
Console.WriteLine("End");
}複製程式碼
這裡我們可以看到最後的執行結果圖使用for迴圈的執行下來都是依次執行。按照相應的順序。但是我們使用Parallel.For()的時候執行下來。也輸出了所有的結果,但是其順序就沒有保證了。
Parallel.ForEach()
我們再看Parallel.ForEach()提供了一個並行處理資料的機制。這裡類似於foreach語句,但是是以一部方式遍歷。這裡沒有確定遍歷的順序,其執行順序也就是不保證的。
#region ForEach 語句比較
public static void ParallelForEach()
{
List<Test> result = new List<Test>();
for (int i = 1; i < 100; i++)
{
Test model = new Test();
model.Name = "Name" + i;
model.Index = "Index" + i;
model.Time = DateTime.Now;
result.Add(model);
}
Parallel.ForEach<Test>(result,s => {
Console.WriteLine(s.Name);
});
}
#endregion 複製程式碼
static void Main(string[] args)
{
ParallelForEach();
}複製程式碼
我們看這裡的執行結果,對資料集合進行了遍歷處理,但是其執行的順序不定,是亂序的結果。這也就是非同步遍歷的一個表現。
ParallelLoopState
下面我們來看ParallelLoopState。它提供了兩個方法。一個是Break、一個是Stop。
Break:表示並行迴圈執行了當前迭代後應儘快停止執行。篩選出符合條件的執行,可能輸出完全。
Stop:表示並行迴圈應儘快停止執行。遇到符合條件後停止並行迴圈,可能不完全輸出。
下面我們看看程式碼:
public static List<Test> GetListTest()
{
List<Test> result = new List<Test>();
for (int i = 1; i < 100; i++)
{
Test model = new Test();
model.Name = i.ToString();
model.Index = "Index" + i;
model.Time = DateTime.Now;
result.Add(model);
}
return result;
}
public static void BraekFor()
{
var result = GetListTest();
Parallel.For(0,result.Count,(int i,ParallelLoopState ls) =>
{
Task.Delay(10).Wait();
if (i > 50)
{
Console.WriteLine("Parallel.For使用Break停止當前迭代:" + i);
ls.Break();
return;
}
Console.WriteLine("測試Parallel.For的Break:" + i);
});
}
public static void StopFor()
{
var result = GetListTest();
Parallel.For(0,ParallelLoopState ls) =>
{
Task.Delay(10).Wait();
if (i > 50)
{
Console.WriteLine("Parallel.For使用Stop停止 迭代:" + i);
ls.Stop();
return;
}
Console.WriteLine("測試Parallel.For的Stop:" + i);
});
}複製程式碼
static void Main(string[] args)
{
BraekFor();
StopFor();
}複製程式碼
我們看對於Parallel.For()來說這個案例。使用Break()停止當前迭代會輸出符合條件所有結果,但是我們使用Stop的時候輸出部分的時候就停止了。
public static void BraekForEach()
{
var result = GetListTest();
Parallel.ForEach<Test>(result,(Test s,ParallelLoopState ls) =>
{
Task.Delay(10).Wait();
if (Convert.ToInt32(s.Name) > 50)
{
Console.WriteLine("Parallel.ForEach使用Break停止當前迭代:" + s.Name);
ls.Break();
return;
}
Console.WriteLine("測試Parallel.ForEach的Break:" + s.Name);
});
}
public static void StopForEach()
{
var result = GetListTest();
Parallel.ForEach<Test>(result,ParallelLoopState ls) =>
{
Task.Delay(10).Wait();
if (Convert.ToInt32(s.Name) > 50)
{
Console.WriteLine("Parallel.ForEach使用Stop停止 迭代:" + s.Name);
ls.Stop();
return;
}
Console.WriteLine("測試Parallel.ForEach的Stop:" + s.Name);
});
}複製程式碼
static void Main(string[] args)
{
BraekForEach();
StopForEach();
}複製程式碼
我們再對Parallel.ForEach進行測試,發現對於Stop和Break的用法和意義是一樣的。
Parallel.Invoke()
上面我們介紹了Parallel.For和Parallel.ForEach以及提供的兩個方法Break和Stop。上面介紹的這些都是對資料的並行處理執行。下面我們介紹Parallel.Invoke()。它是針對於任務的並行執行處理。
這裡我們需要注意以下幾點:
1、如果我們傳入4個任務並行,那麼我們至少需要四個邏輯處理核心(硬體執行緒)才可能使四個任務一起執行。但是當其中一個核心繁忙,那麼底層的排程邏輯就可能會延遲某些方法的初始化執行。
2、Parallel.Invoke()所包含的並行任務不能相互依賴,因為執行執行的順序不可保證。
3、使用Parallel.Invoke()我們需要測試執行結果,觀察邏輯核心使用率以及實現加速。
4、使用Parallel.Invoke()會產生一些額外的開銷,例如分配硬體執行緒。
我們看下面的案例:
下面我們對一個集合的資料進行新增然後輸出。下面我們分為四組測試。500條資料和1000條資料各兩個,分別是一般的同步任務和Parallel.Invoke()的並行任務執行。再觀察其執行的時間比較。
#region Parallel.Invoke()使用共同資源
public static List<Test> _tests = null;
public static void TaskFive_One()
{
for (int i = 0; i < 500; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskFive_One 500條資料第一個方法 執行完成");
}
public static void TaskFive_Two()
{
for (int i = 500; i < 1000; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskFive_Two 500條資料第二個方法 執行完成");
}
public static void TaskFive_Three()
{
for (int i = 1000; i < 1500; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskFive_Three 500條資料第三個方法 執行完成");
}
public static void TaskFive_Four()
{
for (int i = 1500; i < 2000; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskFive_Four 500條資料第四個方法 執行完成");
}
public static void TaskOnethousand_One()
{
for (int i = 0; i < 1000; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskOnethousand_One 1000條資料第一個方法 執行完成");
}
public static void TaskOnethousand_Two()
{
for (int i = 1000; i < 2000; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskOnethousand_Two 1000條資料第二個方法 執行完成");
}
public static void TaskOnethousand_Three()
{
for (int i = 2000; i < 3000; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskOnethousand_Three 1000條資料第三個方法 執行完成");
}
public static void TaskOnethousand_Four()
{
for (int i = 3000; i < 4000; i++)
{
Test test = new Test();
test.Name = i.ToString();
test.Index = i.ToString();
test.Time = DateTime.Now;
_tests.Add(test);
}
Console.WriteLine("TaskOnethousand_Three 1000條資料第四個方法 執行完成");
}
#endregion複製程式碼
static void Main(string[] args)
{
//五百條資料順序完成
Stopwatch swFive = new Stopwatch();
swFive.Start();
Thread.Sleep(3000);
_tests = new List<Test>();
TaskFive_One();
TaskFive_Two();
TaskFive_Three();
TaskFive_Four();
swFive.Stop();
Console.WriteLine("500條資料 順序程式設計所耗時間:" + swFive.ElapsedMilliseconds);
//五百條資料並行完成
Stopwatch swFiveTask = new Stopwatch();
swFiveTask.Start();
Thread.Sleep(3000);
_tests = new List<Test>();
Parallel.Invoke(() => TaskFive_One(),() => TaskFive_Two(),() => TaskFive_Three(),() => TaskFive_Four());
swFiveTask.Stop();
Console.WriteLine("500條資料 並行程式設計所耗時間:" + swFiveTask.ElapsedMilliseconds);
//一千條資料順序完成
Stopwatch swOnethousand = new Stopwatch();
swOnethousand.Start();
Thread.Sleep(3000);
_tests = new List<Test>();
TaskOnethousand_One();
TaskOnethousand_Two();
TaskOnethousand_Three();
TaskOnethousand_Four();
swOnethousand.Stop();
Console.WriteLine("1000條資料 順序程式設計所耗時間:" + swOnethousand.ElapsedMilliseconds);
//一千條資料並行完成
Stopwatch swOnethousandTask = new Stopwatch();
swOnethousandTask.Start();
Thread.Sleep(3000);
_tests = new List<Test>();
Parallel.Invoke(() => TaskOnethousand_One(),() => TaskOnethousand_Two(),() => TaskOnethousand_Three(),() => TaskOnethousand_Four());
swOnethousandTask.Stop();
Console.WriteLine("1000條資料 並行程式設計所耗時間:" + swOnethousandTask.ElapsedMilliseconds);
}複製程式碼
我們看這次的執行結果,發現我們使用順序程式設計和並行程式設計所需要的時間相差無幾的。那麼怎麼回事呢?我們仔細檢查下,發現我們似乎對資源進行了共享。我們下面處理下,對list集合不進行共享看看。
#region Parallel.Invoke()不使用共同資源
public static void TaskFive_One()
{
List<Test> tests = new List<Test>();
for (int i = 0; i < 500; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskFive_One 500條資料第一個方法 執行完成");
}
public static void TaskFive_Two()
{
List<Test> tests = new List<Test>();
for (int i = 500; i < 1000; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskFive_Two 500條資料第二個方法 執行完成");
}
public static void TaskFive_Three()
{
List<Test> tests = new List<Test>();
for (int i = 1000; i < 1500; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskFive_Three 500條資料第三個方法 執行完成");
}
public static void TaskFive_Four()
{
List<Test> tests = new List<Test>();
for (int i = 1500; i < 2000; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskFive_Four 500條資料第四個方法 執行完成");
}
public static void TaskOnethousand_One()
{
List<Test> tests = new List<Test>();
for (int i = 0; i < 1000; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskOnethousand_One 1000條資料第一個方法 執行完成");
}
public static void TaskOnethousand_Two()
{
List<Test> tests = new List<Test>();
for (int i = 1000; i < 2000; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskOnethousand_Two 1000條資料第二個方法 執行完成");
}
public static void TaskOnethousand_Three()
{
List<Test> tests = new List<Test>();
for (int i = 2000; i < 3000; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskOnethousand_Three 1000條資料第三個方法 執行完成");
}
public static void TaskOnethousand_Four()
{
List<Test> tests = new List<Test>();
for (int i = 3000; i < 4000; i++)
{
Test test = new Test();
test.Name = "Name" + i.ToString();
test.Index = "Index" + i.ToString();
test.Time = DateTime.Now;
tests.Add(test);
}
Console.WriteLine("TaskOnethousand_Four 1000條資料第四個方法 執行完成");
}
#endregion複製程式碼
static void Main(string[] args)
{
Stopwatch swTest = new Stopwatch();
swTest.Start();
Thread.Sleep(3000);
TaskFive_One();
TaskFive_Two();
TaskFive_Three();
TaskFive_Four();
swTest.Stop();
Console.WriteLine("500條資料 順序程式設計所耗時間:" + swTest.ElapsedMilliseconds);
//五百條資料並行完成
swTest.Restart();
Thread.Sleep(3000);
Parallel.Invoke(() => TaskFive_One(),() => TaskFive_Four());
swTest.Stop();
Console.WriteLine("500條資料 並行程式設計所耗時間:" + swTest.ElapsedMilliseconds);
//一千條資料順序完成
swTest.Restart();
Thread.Sleep(3000);
TaskOnethousand_One();
TaskOnethousand_Two();
TaskOnethousand_Three();
TaskOnethousand_Four();
swTest.Stop();
Console.WriteLine("1000條資料 順序程式設計所耗時間:" + swTest.ElapsedMilliseconds);
//一千條資料並行完成
swTest.Restart();
Thread.Sleep(3000);
Parallel.Invoke(() => TaskOnethousand_One(),() => TaskOnethousand_Four());
swTest.Stop();
Console.WriteLine("1000條資料 並行程式設計所耗時間:" + swTest.ElapsedMilliseconds);
}複製程式碼
我們看下我們修改共享資源後,對於500條資料的執行結果,順序程式設計比並行程式設計還是要快點,但是在1000條資料的時候並行程式設計就明顯比順序程式設計要快了。而且在測試中並行程式設計的執行順序也是不固定的。我們在日常程式設計中我們需要衡量我們的應用是否需要並行程式設計,不然可能造成更多的效能損耗。
世界上那些最容易的事情中,拖延時間最不費力。堅韌是成功的一大要素,只要在門上敲得夠久夠大聲,終會把人喚醒的。
歡迎大家掃描下方二維碼,和我一起學習更多的知識?