.NET4.0並行計算技術基礎(11)

 
今天終於在MSDN看到,微軟將於2009年10月21日向公衆開放VS2010 BTEA2的下載,受夠了VS2010 BTEA1的不穩定與緩慢的速度,對新版本指望好久了,但願BETA2可以修正BETA1中巨多的BUG,成爲一個成熟穩定的開發平臺。
 
今天貼出PLINQ部分的內容,也許等BETA2發佈後,我得動手修改個人文章了,不過我估計在基類庫方面應該不會有大的變化,而大的變化應該集中於開發工具VS 2010自己,因此,本系列文章中介紹基本技術與原理是不會有什麼過期的。
 
PLINQ部分將分兩次貼出。
 
                                              金旭亮
                                          2009.10.20
 
=================================================
.NET4.0並行計算技術基礎(11)
 
這是一個系列講座,前面幾講的連接爲:
 
 
=======================================================
 

19.1 讓查詢執行得更快——Parallel LINQ

         LINQ的出現對於.NET平臺而言是一件大事,它使用一種統一的模式查詢數據,而且能夠緊密地與具體編程語言直接集成。LINQ語句的編寫方式是「動態組合」和「遞歸」的,這與函數式編程語言(如F#)相似,這種編寫方式的優勢在於代碼量小,經過動態組合一些典型的查詢運算符,能夠實現至關複雜的數據處理邏輯,而一樣的功能若是採用傳統的編碼方式實現,將耗費很多的力氣寫代碼。
         .NET 4.0引入的PLINQLINQ的「升級換代」技術,它容許以並行方式執行LINQ查詢。
         使用PLINQ技術的最大好處之一是當計算機處理器個數增長時,不須要修改(或僅需少許修改)源代碼,程序性能就能夠獲得相應的提高。
         在本節中,咱們先介紹PLINQ與其餘技術的關係,而後介紹如何編寫PLINQ查詢,最後,剖析PLINQ內部的工做原理。
 
交叉連接:
要學習本節,要求讀者掌握了LINQ編程的基本技巧。本書第24章詳細介紹LINQ,可供讀者參閱
 

19.4.1 PLINQ概述

         PLINQ主要用於並行執行數據查詢,而它自己又是.NET 4.0所引入的並行擴展的有機組成部分,所以,它與LINQTPL都有着密切的聯繫。
         LINQ,是英文詞組「Language-Integrated Query 的縮寫,中文譯爲「語言集成的查詢」,分爲LINQ to ObjectLINQ to SQLLINQ to XMLLINQ to DataSet等幾個有機組成部分。
         在目前的版本中,PLINQ只實現了LINQ to Object的並行執行,換句話說,PLINQ實現了對「內存」中的數據進行並行查詢。若是數據來自於數據庫或文件,您須要將這些數據加載到內存中才能使用PLINQ
         標準的LINQ查詢運算符是由「System.Linq.Enumerable」類所封裝的擴展方法實現的,相似地,PLINQ也爲全部標準的LINQ查詢運算符(如whereselect等)提供了並行版本,這些並行的PLINQ查詢運算符實現爲.NET 4.0新增的「System.Linq.ParallelEnumerable」類的擴展方法。
        
交叉連接:
       本書3.2.4小節介紹了擴展方法,擴展方法在LINQ中有着重要的應用,本書第23章介紹了這方面的內容。
 
         LINQ查詢轉換爲PLINQ很是簡單,在許多狀況下只需簡單地添加一個AsParallel子句就好了,例如,如下代碼將把整數集合中的偶數挑出來:
 
          //建立一個100個元素的整數集合,保存從1100的整數.
            var source = Enumerable.Range(1, 100);
            var evenNums = from num in source.AsParallel()
                           where num % 2==0
                           select num;
 
         能夠看到,PLINQ查詢除了多一個AsParallel子句以外,與標準LINQ的查詢並無什麼不一樣,原有的絕大多數LINQ編程方法仍然繼續適用。
         .NET語言編譯器「看到」一個查詢中包含AsParallel子句代碼時,它會在編譯期間引用System.Concurrency.dll程序集,將相應的標準LINQ查詢運算符替換爲對ParallelEnumerable類相應靜態方法的調用,同時「悄悄地」將查詢的返回值修改成相應的並行版本(好比許多PLINQ查詢返回一個ParallelQuery<T>類型的數據集合)。因爲ParallelQuery<T>派生自IEnumerable<T>,然後者是許多標準LINQ查詢運算符的返回數據類型,所以,PLINQ利用多態性保證了它與原有LINQ代碼的最大兼容性。
         LINQ相似,PLINQ也具備「延遲執行」的特性,只有對查詢集合調用foreach迭代、或者調用ToList之類方法時,PLINQ查詢纔會真正執行。
         設計者在設計PLINQ,追求的一個目標是:PLINQ毫不能比它的前輩--LINQ to Object運行得更慢!若是在某個地方作不到,它就採用串行方式執行。
         在真實的應用程序中,要肯定到底性能有無提高,請直接運行LINQPLINQ的兩個版本進行對比測試以決定取捨。
         通常來講,對於小數據量的數據集而言,優先選擇LINQ而不是PLINQ
 
         提示:
       若是須要的話,可使用AsSequential子句「強制」PLINQ查詢採用串行方式執行。甚至能夠在同一條查詢語句中混用「並行」與「串行」兩種模式。
 
         另外,PLINQ在底層使用TPL所提供的基礎架構完成全部工做,所以,PLINQ是比「Task」抽象層次更高的編程手段,在實際開發中,只要可能,推薦直接使用PLINQ
         總之,在設計並行程序時,推薦按照如下順序來設計技術解決方案:
         基於PLINQ的聲明式編程方式à使用Task的直接基於TPL的「任務並行」編程方式à使用線程的基於CLR的「多線程」編程方式
 

19.4.2 基於PLINQ開發

         因爲 PLINQ 創建於 LINQ 基礎之上,實現了全部標準 LINQ 運算符的並行版本,所以,本節只介紹 PLINQ 中不一樣於 LINQ 的技術特徵。讀者須要先掌握好編寫標準 LINQ 查詢語句的基本技巧,才能夠掌握本節介紹的內容。

1 LINQ查詢轉爲並行執行

         LINQ 查詢語句中,在一個能夠返回數據集合的子句後面大均可以添加一個 AsParallel() AsParallel<T>() 子句將其轉換爲 PLINQ 語句。
         如下是一個例子,從一個整數集合中取出全部的偶數
        
    List<int> lst = new List<int>();
    //lst中追加數據,代碼略...
    var evenNums = from num in lst.AsParallel<int>()
                           where num%2==0
                           select num;
 
         上述代碼執行時, TPL 引擎會自動在後臺建立並管理線程,讓查詢得以並行執行。
         然而,在某些狀況下,因爲並行處理會帶來錯誤的結果,所以必須強制將其轉爲串行模式,這時,能夠調用 ParallelEnumerable 類的擴展方法 AsSequential() 達到此目的。
         請看示例 AsParallelAndAsSequential
         示例程序在一個保存了學生考試成績的數據集合中查找 60 分以上的學生,而且按照成績高低排名次。
         若是使用 PLINQ 來完成這個工做,咱們能夠寫出如下代碼:
 
    int counter = 0;//計數器
     var query =
                from student in students.AsParallel()
                where student.Score > 60              //分數大於60
                orderby student.Score descending      //按成績降序排序
                select new                            //返回學生信息
                {
                    TempID = ++counter,    //學生成績名次
                    student.Name,
                    student.Score
                };
     //輸出處理結果,代碼略……
 
         請注意上述代碼中加了方框的部分,因爲上述 PLINQ 查詢是並行執行的,這就是說會有多個線程同時訪問 counter 變量,這就隱含着一個「多線程同時訪問共享資源」的問題,並且到底 TPL 會建立多少個線程,以及這些線程的推動順序是不可控的,所以,上述代碼執行結果是錯誤的,學生成績與名次不能正確對應。
         若是將上述代碼中的 AsParallel 子句去掉,則結果正確,但卻失去了並行執行的任何好處。
         如何能在享受 PLINQ 所帶來的性能提高的好處的同時,又能避免因並行執行而獲得錯誤的結果?
         其關鍵在於要分清楚數據處理工做中哪些能夠並行執行,哪些必須串行執行,而後,再將其組合起來。
         對於這種須要混合並行與串行執行的狀況,直接使用 PLINQ 語句比較困難,一般在這種場景下使用擴展方法實現。
         在本例中,能夠寫出如下代碼同時組合「並行執行」與「串行執行」的兩種數據處理工做。
 
    var query2 = students.AsParallel()        //使用並行查詢
                .Where(student => student.Score > 60) //分數大於60
                .OrderByDescending(stu => stu.Score)  //按成績降序排序
                .AsSequential()                       //強制轉換爲串行執行
                .Select(studentInfo =>
                new
                {
                    TempID = ++counter,  //名次
                    studentInfo.Name,
                    studentInfo.Score
                });
 
         上述代碼中,按成績篩選記錄是並行執行的,而生成處理結果集合時是順序執行的。

2 維持數據的順序

         默認狀況下, PLINQ 查詢要處理的數據被認爲「順序可有可無」, TPL 會按照它內置的算法將數據分紅幾組(稱爲「數據分區」),而後在這些相互獨立的數據分區上並行處理。
         然而在某些狀況下,數據的順序是重要的,請看示例項目 AsOrdered
         示例項目先用隨機整數填充了一個 List<int> 集合對象 source ,而後,程序找出排在最前面的 10 個偶數出來,要求保持原有順序。
         如下 PLINQ 查詢完成這個工做。
        
            var parallelQuery = from num in source.AsParallel().AsOrdered()
                                where num % 2 == 0
                                select num;
            var First10Numbers = parallelQuery.Take(10);
 
         上述查詢語句中的 AsOrdered() 子句強制 PLINQ 保持原始數據的排列次序。
         讀者能夠試一下,若是去掉 AsOrdered() 子句,則獲得的結果是錯誤的。
         AsOrdered() ParallelEnumerable 類提供的靜態擴展方法,所以適用於絕大多數數據集合類型。
 
      注意:
       AsOrdered() 和前面介紹的AsSequential()是不同的,AsSequential()強制PLINQ查詢以串行方式執行,而AsOrdered()還是並行執行的,只不過並行執行的結果先被緩存起來,而後再按原始數據順序進行排序,才獲得最後的結果。
        
         很明顯,給 PLINQ 查詢加上 AsOrdered() 子句將會影響到程序的性能,所以,儘可能避免使用它。
         在一些狀況下,能夠經過修改 PLINQ 查詢的順序避免使用 AsOrdered() 子句。例如,假設整數集合中的原始是排好序的,則如下 PLINQ 查詢按順序取出全部的偶數:
 
var  evenNums = from num in source.AsParallel().AsOrdered()
where num % 2 == 0
select num;
 
         若是對查詢操做的順序進行一下修改,會獲得更好的性能:
 
var evenNums = from num in source.AsParallel()
where num % 2 == 0
orderby num
select num;
 
         固然,咱們並不能確定「修改以後的代碼必定比修改前快」,由於這取決於許多因素,特別是「 TPL 執行 PLINQ 查詢內部所使用的數據分區策略和並行算法」,它對於應用軟件開發工程師而言是不可控的因素,但卻對性能影響很大。此處只是提醒讀者在編碼時須要注意這些細節。
 
===============================================
相關文章
相關標籤/搜索