摘要:[Visual Studio 2010]TPL平行運算 - 使用Parallel.For來處理影像
在Visual Studio 2010提供了更多更強大的功能以及新的函式庫,例如軟體工程的整合、雲端、平行運算、MEF開放架構等,不過在平行運算的技術方面,.NET 4.0提供了Task Parallel Library(TPL)的函式庫,此函式庫主要是用在Manage Code上面,當然在C++方面微軟也有提供另一個Parallel Pattern Library(PPL)的函式庫,也支援新一代的C++ 0x(參考MSDN)。在.NET 3.5的時候其實TPL就已經開始發展中,當時是以Parallel Extension的方式來擴充(在Codeplex上,不知道有沒有記錯),不過到了4.0才正式納入.NET Framework當中。
微軟的TPL函式庫基本上都是由執行緒所建立而成,並且幫我們處理掉許多並行執行以及多執行緒的相關問題,這樣在使用平行運算的時候,就可以避免到一些執行緒的衝突或是需要作鎖定等,目前所知的平行運算大致分為三種:
- 任務平行(Task Parallelism):基本上每個任務會以System.Threading.Tasks.Task的類別來表示,不再是使用Thread來代表,並且Task類別都需要傳入Action的委派,這樣可以告知此任務要執行哪些動作,例如new Task(() => DoSomething());,所以每一個Task都是一的執行單元,並且可以透過Start()方法來啟動任務。
- 資料平行(Data Parallelism):通常處理的項目會是一個集合或是陣列(由多個單元所組成的資料),例如我們常用for或foreach來處理集合內的資料,不過for只能循序的存取資料,並沒有辦法充分運用多核心的處理器,導致在讀取大量的資料時,只用到CPU其中一個核心作處理,所以在TPL提供了好用的類別System.Threading.Tasks.Parallel,Parallel提供了一些靜態方法,例如Parallel.For、Parallel.ForEach,這兩個都是用來執行回圈的動作,但是差別在於只要是透過Parallel.For或是Parallel.ForEach的方法執行,就會以平行的方式來處理資料。
- PLINQ(Parallel Linq):LINQ在C# 3.0的語法大放異彩,改變很多人對於程式語言的寫作方式,而且LINQ也可以透過LINQ Provider來擴充,讓所有的資料存取都可以使用linq語法來查詢。不過在.NET 4.0提供了平行的LINQ Provider,並且透過擴充方法(Extension Method)的方式來對IEnumerable<T>型別擴充,提供了AsEnumerable()的方法(定義在System.Linq.ParallelEnumerable類別中),例如下面的範例是來自MSDN:
var source = Enumerable.Range(100, 20000); // Result sequence might be out of order. var parallelQuery = from num in source.AsParallel() where num % 10 == 0 select num;
上面提到了許多關於平行運算的簡介,開始要來執行今天的任務,這次想要將以前寫的灰階轉換程式透過TPL來處理,下面的範例是之前寫的程式碼片段:
/// /// 轉換成灰階圖檔 /// public static Bitmap ConvertToGrey(string filePath) { //來源圖檔 Bitmap bmp = new Bitmap(filePath); //建立新的Bitmap物件,用來存放轉換後的圖片 Bitmap newBmp = new Bitmap(bmp.Width, bmp.Height, bmp.PixelFormat); BitmapData newData = newBmp.LockBits(new Rectangle(0, 0, newBmp.Width, newBmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat); BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat); //循序轉換 for (int i = 0; i < bmp.Height; i++) { for (int j = 0; j < bmp.Width; j++) { int offset = i * bmpData.Stride + j * (bmpData.Stride / bmpData.Width); //直接在記憶體讀寫像素的RGB值 int r = Marshal.ReadByte(bmpData.Scan0, offset + 2); int g = Marshal.ReadByte(bmpData.Scan0, offset + 1); int b = Marshal.ReadByte(bmpData.Scan0, offset); int grey = (r * 77 + g * 151 + b * 28) >> 8; Marshal.WriteByte(newData.Scan0, offset, (byte)grey); Marshal.WriteByte(newData.Scan0, offset + 1, (byte)grey); Marshal.WriteByte(newData.Scan0, offset + 2, (byte)grey); } } newBmp.UnlockBits(newData); bmp.UnlockBits(bmpData); return newBmp; }
由於處理的是一張影像圖檔,所以我改用Parallel.For的方式來讓資料平行處理,來看看效能是否變好,下面是修改後的程式碼,其實跟原來的差不多,只是將for回圈改成Parallel.For而已:
/// /// 透過TPL轉換成灰階圖檔 /// public static Bitmap ConvertToGreyWithParallel(string filePath) { //來源圖檔 Bitmap bmp = new Bitmap(filePath); //建立新的Bitmap物件,用來存放轉換後的圖片 Bitmap newBmp = new Bitmap(bmp.Width, bmp.Height, bmp.PixelFormat); BitmapData newData = newBmp.LockBits(new Rectangle(0, 0, newBmp.Width, newBmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat); BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat); int height = bmp.Height; int width = bmp.Width; //平行轉換 Parallel.For(0, height, i => Parallel.For(0, width, j => { int offset = i * bmpData.Stride + j * (bmpData.Stride / bmpData.Width); //直接在記憶體讀寫像素的RGB值 int r = Marshal.ReadByte(bmpData.Scan0, offset + 2); int g = Marshal.ReadByte(bmpData.Scan0, offset + 1); int b = Marshal.ReadByte(bmpData.Scan0, offset); int grey = (r * 77 + g * 151 + b * 28) >> 8; Marshal.WriteByte(newData.Scan0, offset, (byte)grey); Marshal.WriteByte(newData.Scan0, offset + 1, (byte)grey); Marshal.WriteByte(newData.Scan0, offset + 2, (byte)grey); })); newBmp.UnlockBits(newData); bmp.UnlockBits(bmpData); return newBmp; }
這兩個程式碼看起來都一樣,只是變成Parallel.For來處理資料, 基本上Parallel.For的基本簽名如下:
public static ParallelLoopResult For(int fromInclusive, int toExclusive, Actionbody);
fromInclusive:起始的計數數值
toExclusive:結束的計數數值
body:要執行的動作,也就是for迴圈內的區塊程式碼
測試結果:
下面的結果分別為透過循序的方式轉換(巢狀for迴圈,圖左)以及平行的方式轉換(Parallel.For,圖右):
測試機器為Lenovo X61,雙核CPU,可以看到時間上差了約3.5倍,不過這也要看圖片的像素編排以及圖檔格式,不過使用平行的方式來轉換,可以發現它的效能是比循序的還要快。
參考資料:
http://msdn.microsoft.com/en-us/concurrency/default.aspx
http://msdn.microsoft.com/en-us/magazine/ee819128.aspx
http://msdn.microsoft.com/en-us/library/dd460693(VS.100).aspx
程式碼範例: