使用Parallel.Invoke並行你的代碼
優勢和劣勢
使用Parallel.Invoke的優勢就是使用它執行很多的方法很簡單,而不用擔心任務或者線程的問題。然而,它並不是適合所有的場景。Parallel.Invoke有很多的劣勢
如果你使用它來啟動那些需要執行很長時間的方法,它將會需要很長時間才能返回。這可能會導致很多的核心在很長時間都保持閑置。因此,使用這個方法的時候測量執行速度和邏輯核心使用率很重要。
它對並行的伸縮性有限制,因為它只能調用固定數目的委托。在前面的例子中,如果你在一個有16個核心的電腦上執行,它將只會並行啟動四個方法。因此,12個邏輯核心仍然保持閑置。
每次使用這個方法執行並行方法之前都會增加額外的開銷。
就像任何的並行代碼,不同的方法之間存在內部依賴和難以控制的交互,會導致難以探測的並行bug和難以預料的副作用。然而,這個劣勢是使用所有的並行代碼,它並不是使用Parallel.Invoke是才存在的問題。
無法保證需要並行的方法的執行順序;因此,Parallel.Invoke並不適合執行那些需要特定執行計划的復雜算法。
使用不同的並行執行計划啟動的任何一個委托都可能拋出異常;因此,捕獲和處理這些異常會比傳統的串行代碼更復雜。
交錯並發和並發
正如你在前邊例子和圖片2-5中所看到的,交錯並發和並發是不同的事情。
交錯並發意味着在重疊的時間段內可以開始、執行和結束不同部分的代碼。交錯並發甚至可以運行在只有一個邏輯核心的電腦上。當很多的代碼交錯運行在只有一個邏輯核心的電腦上時,時間調度策略和快速的上下文切換提供了並行執行的假象。然而,使用這種硬件,交錯執行這些代碼比單獨執行某一端獨立的代碼需要更多的時間,因為這些並發的代碼是需要競爭硬件資源的。你可以講交錯並行想象成多輛卡車共享同一條車道。這也就是為什么交錯並發也被定義為一種虛擬的並行。
並發意味着不同的代碼可以同時執行,充分利用底層硬件的並發處理能力。真正的並發是不能發生在只有一個邏輯核心的計算機上。為了執行並行代碼你至少需要兩個邏輯核心。當真正的並發發生的時候是可以提升執行速度的,因為並行執行代碼可以減少完成特定算法必須的時間開銷。前邊的圖中提供了如下的可能的並發場景:
並發,四個邏輯核心的理想並行—在這種理想的情形下,四個方法的指令分別執行在一個不同的邏輯核心上。
結合交錯並發和並發;並不完美的並行四個方法只能利用兩個邏輯核心—有時,四個方法的執行並行運行在不同的邏輯核心上,有時它們必須等待它們的時間片。在這種情況下,交錯並發結合了真正的並行。這是最普遍的情形,因為即使是在實時操作系統中,也確實很難實現完美的並行。
圖 2-5
循序代碼轉化為並行代碼
在過去十年,大多的C#編寫的代碼是順序和同步執行的。因此,很多的算法的思想既沒有並發也沒有並行。大多的時間,你很難發現可以完整的轉換成完全並行和完美伸縮性代碼的方法。即使可以找到,但是這並不是最普遍的場景。
當你有並行代碼並想利用潛在的並行來提升執行速度的時候,你必須找到可以並行的熱點區域。然后,你可以將他們轉化成並行代碼,測試執行速度,確定潛在的伸縮性,並且確保在將現存順序代碼轉換成並行代碼的時候沒有引入新的bug。
探測並行熱點
列表2-3展示了一個例子,這是一個執行了如下兩個順序的方法的簡單的控制台應用程序。
GenerateAESKeys—這個方法執行一個for循環,並根據指定的常量字段NUM_AES_KEYS產生相應數目的AES鍵。它使用System.Security.Cryptography.AesManaged類提供的GenerateKey方法。一旦產生了這個鍵,就會將這個byte數據轉化成十六進制字符串形式,並將轉換的結果保存在局部變量hexString里。
GenerateMD5Hashes—這個方法執行一個for循環,並使用md5算法根據給定的常量NUM_MD5_HASHES來產生相應數目的哈希值。它使用用戶名作為參數調用System.Security.Cryptography.MD5類提供的ComputeHash方法。一旦產生了哈希值,就會將這個byte數組轉換成十六進制的字符串形式,並使用局部變量hexString保存。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// Added for the Stopwatch
using System.Diagnostics;
// Added for the cryptography classes
using System.Security.Cryptography;
// This namespace will be used later to run code in parallel
using System.Threading.Tasks;
namespace Listing2_3
{
class Program
{
private const int NUM_AES_KEYS = 800000;
private const int NUM_MD5_HASHES = 100000;
private static string ConvertToHexString(Byte[] byteArray)
{
// Convert the byte array to hexadecimal string
var sb = new StringBuilder(byteArray.Length);
for ( int i = 0; i < byteArray.Length; i++)
{
sb.Append(byteArray[i].ToString( " X2 "));
}
return sb.ToString();
}
private static void GenerateAESKeys()
{
var sw = Stopwatch.StartNew();
var aesM = new AesManaged();
for ( int i = 1; i <= NUM_AES_KEYS; i++)
{
aesM.GenerateKey();
byte[] result = aesM.Key;
string hexString = ConvertToHexString(result);
// Console.WriteLine(“AES KEY: {0} “, hexString);
}
Debug.WriteLine( " AES: " + sw.Elapsed.ToString());
}
private static void GenerateMD5Hashes()
{
var sw = Stopwatch.StartNew();
var md5M = MD5.Create();
for ( int i = 1; i <= NUM_MD5_HASHES; i++)
{
byte[] data =
Encoding.Unicode.GetBytes(
Environment.UserName + i.ToString());
byte[] result = md5M.ComputeHash(data);
string hexString = ConvertToHexString(result);
// Console.WriteLine(“MD5 HASH: {0}”, hexString);
}
Debug.WriteLine( " MD5: " + sw.Elapsed.ToString());
}
static void Main( string[] args)
{
var sw = Stopwatch.StartNew();
GenerateAESKeys();
GenerateMD5Hashes();
Debug.WriteLine(sw.Elapsed.ToString());
// Display the results and wait for the user to press a key
Console.ReadLine();
}
}
}
方法GenerateAESKeys中的for循環在代碼里沒有使用它的控制變量i,因為它僅僅控制產生一個隨機AES key的次數。然而,在方法GenerateMD5Hashes中將它的控制變量i添加到計算機用戶名后邊。然后,使用這個字符串作為調用產生哈希值的方法的輸入數據,具體的代碼如下代碼所示
{
byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + i.ToString());
byte[] result = md5M.ComputeHash(data);
string hexString = ConvertToHexString(result);
// Console.WriteLine(hexString);
}
清單2-3中的高亮代碼行是測量每個方法執行的總時間。它通過在每個方法開始的時候調用StartNew方法來開始一個新的StopWatch,然后最終將消耗的時間寫到Debug的輸出中。
清單2-3中也展示了那些被注釋掉的輸出產生的key和哈希值的語句,因為這些操作會將字符串發送到控制台,這將會產生性能瓶頸影響時間測試的准確性。
圖2-6展示了這個程序的順序執行流程和在一個有雙核微處理器的計算機上執行前邊兩個方法所分別使用的時間。
兩個方法需要執行將近14秒。第一個方法執行8s,后一個需要6s。當然,這些消耗的時間會隨底層的硬件配置產生很大的變化。這兩個方法之間沒有進行任何的交互;因此,他們之間是完全的相互獨立的。就這樣一個接着一個的順序執行,是不能利用令外一個核心提供的並行處理能力的。因此,這兩個方法就是一個並行的熱區,在這里並行可以幫助我們完成一個重大的執行速度的提升。例如,你可以使用Parallel.Invoke並行的執行這兩個方法。
圖 2-6
測量並行的執行速度提升
使用下邊的新版本替換列表2-3中的Main方法,這里使用Parallel.Invoke來並行啟動兩個方法。
{
var sw = Stopwatch.StartNew();
Parallel.Invoke(
() => GenerateAESKeys(),
() => GenerateMD5Hashes());
Debug.WriteLine(sw.Elapsed.ToString());
// Display the results and wait for the user to press a key
Console.WriteLine(“Finished!”);
Console.ReadLine();
}
圖2-7展示了這個程序的新版本的並行執行流程和兩個方法在使用雙核微處理器的計算機上執行消耗的時間。
現在兩個方法執行將近9m,因為他們利用了微處理器提供的兩個核心。因此,你可以使用下邊的公式計算其可以實現的速度提升:
Speedup = (Serial execution time) / (Parallel execution time)
14 / 9 = 1.56x
圖 2-7
正如你所看到的,GenerateAESKeys比GenerateMD5Hashes消耗了更長的時間:9:6。然而,如果所有的委托沒有都執行結束,Parallel.Invoke是不會執行下邊的代碼的。因此,最后的3s,應用程序並沒有利用每一個核心,存在一個load-imbalance的問題,如圖2-8所示。
圖 2-8
如果這個應用程序運行在一個有四核微處理器的計算機上,它的速度提升幾乎是一樣的,因為它不能調度使用底層硬件的另外兩個核心。
在這個例子中,你檢測並行的熱點,並添加一些代碼來測試執行特定方法消耗的時間。然后,你可以僅僅改變幾行代碼完成一個有趣的執行速度提升。當在數據並行場景中可用核心的數目增加時,你現在需要知道命令數據並行TPL結構來實現一個更好的結果並改善伸縮性。
理解並行執行
接下來,你需要解除在方法GenerateMD5Hashes和GenerateAESKeys中注釋的有關向控制台輸出的代碼行:
Console.WriteLine(“AESKEY: {0} “, hexString);
Console.WriteLine(“MD5HASH: {0}”, hexString);
向控制台輸出對並行執行會產生性能瓶頸。然而,這次,現在不需要測試准確的時間。相反,你也可以看到並行執行的兩個方法的輸出結果。列表2-4展示了這個程序產生的一個控制台輸出的例子。列表中高亮的短十六進制字符串是相應的MD5哈希。其他的十六進制字符串展示的是AES key。每一個AES key消耗的時間比每個md5哈希要短。記住那段代碼產生了800000 AES key和100000 MD5哈希。
List2-4
現在,注釋掉這兩個方法中那些輸出結果到控制台的代碼。