[VS2010]TPL平行運算 - 使用Parallel.For來處理影像

摘要:[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函式庫基本上都是由執行緒所建立而成,並且幫我們處理掉許多並行執行以及多執行緒的相關問題,這樣在使用平行運算的時候,就可以避免到一些執行緒的衝突或是需要作鎖定等,目前所知的平行運算大致分為三種:

  1. 任務平行(Task Parallelism):基本上每個任務會以System.Threading.Tasks.Task的類別來表示,不再是使用Thread來代表,並且Task類別都需要傳入Action的委派,這樣可以告知此任務要執行哪些動作,例如new Task(() => DoSomething());,所以每一個Task都是一的執行單元,並且可以透過Start()方法來啟動任務。
  2. 資料平行(Data Parallelism):通常處理的項目會是一個集合或是陣列(由多個單元所組成的資料),例如我們常用for或foreach來處理集合內的資料,不過for只能循序的存取資料,並沒有辦法充分運用多核心的處理器,導致在讀取大量的資料時,只用到CPU其中一個核心作處理,所以在TPL提供了好用的類別System.Threading.Tasks.Parallel,Parallel提供了一些靜態方法,例如Parallel.For、Parallel.ForEach,這兩個都是用來執行回圈的動作,但是差別在於只要是透過Parallel.For或是Parallel.ForEach的方法執行,就會以平行的方式來處理資料。
  3. 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, Action body);

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

程式碼範例:

ConvertToGrey.rar