Win32 多執行緒的效能(1) (轉)

gugu99發表於2008-01-08
Win32 多執行緒的效能(1) (轉)[@more@]

 

作者:公司供稿 Ruediger R. Asche
Microsoft Developerwork 技術小組
摘要


  本文討論將單執行緒應用重新編寫成多執行緒應用程式的策略。它以Microsoft? ? 95和?的平臺為例,從吞吐量(throughput)和響應方面,與相容的單執行緒計算相比較而分析了多執行緒計算的。


介紹


  在您所能夠找到的有關多執行緒的資料中,多數都是講述同步概念的。例如,如何化(serialize)共享公共資料的執行緒。這種把重點放在討論同步上是有意義的,因為同步是多執行緒中不可缺少的一部分。本文則後退了一步(takes a step back),主要討論有關多執行緒中很少有人涉及的一面:決定一個計算如何能夠被有意義地拆分為多個執行緒。本文中所使用的示例程式,THRDPERF,在Microsoft? Windows? 95和Windows NT? 兩個平臺之上,針對同一個計算採取序列和併發兩種方法分別實現了測試套件(test suite),並從吞吐量和效能兩方面來比較它們。

  本文的第一部分建立了一些有關多執行緒應用程式的詞彙(vocabulary),討論測試套件的範圍,並且介紹了示例程式套件是如何設計的。第二部分討論測試的結果,並且包括對於多執行緒應用的建議。與之相關的文章 "Interacting with Microsoft : A Case Study in OLE Automation" 討論有關該示例程式套件的一個有趣的問題,即使用測試集合所獲得的資料是如何使用 OLE Automation被輸入 Microsoft Excel 中的。

  如果您是豐富的多執行緒應用程式程式設計者,您可以跳過介紹部分,而直接到下面的“結果”部分。


多執行緒詞彙


  很長一段時間以來,您的應用程式一直被使用——它運轉出色,是可以信賴的,而且 the whole bit——但它十分的遲緩,並且您有如何利用多執行緒的想法。但是,在開始這樣做之前請稍等一會兒,因為這裡有許多的陷阱,它們使您相信某種多執行緒設計是非常完美的,但實際上並不是這樣。

  在您跳至有關要進入的結論之前,首先讓我們澄清一下在本文中將不討論的內容:


在 Microsoft ? 應用程式程式設計介面()下提供多執行緒訪問的庫是不同的,但是我們不關注這一問題。示例程式套件,Threadlib.exe,是在一個Microsoft Foundation Class Library (MFC)應用程式中使用Win32多執行緒API來編寫的,但是,您是使用Microsoft C執行時(CRT)庫、MFC庫,還是單純的(barebones) Win32 API來建立和維持執行緒,我們並不關心。

  實際上,每一種庫最後都要 Win32 服務CreateThread來建立一個工作執行緒,並且多執行緒本身總是要透過來。您想要使用哪一種包裝機制將不會影響本文的論題。當然,您是使用某一個還是使用其它的包裝庫(wrapper library),可能會引起效能上的差異,但是在這兒,我們主要討論多執行緒的本質,而不關心其包裝(wrapper)。


本文所討論的是在單機器上執行的多執行緒應用程式。多處理器則是一個完全不同的主題,並且本文中所討論的結論,幾乎沒有一個可以應用於多處理器的機器中。我還沒有這樣的機會在一個執行 Windows NT 系統的可調整的(scalable)對稱多執行緒(SMP)機器上執行該示例。如果您有這樣的機會,我非常高興地希望知道您的結果。


在本文中,我更喜歡一般性地引用“計算”。計算被定義為您的應用程式的一個子任務,可以被作為整體或部分來執行,可以早於或遲於另一個計算,或者與其他的計算同時發生。例如,讓我們假設某個應用程式需要的資料,並且需要儲存這些資料到。我們可以假定輸入資料包含一種計算,而儲存這些資料則是另一種計算。根據應用程式的計算的設計,下面兩種情況都是可能的:一種是資料的儲存和新資料的輸入是同時交叉進行的;另一種是直到使用者已經輸入了全部的資料才可是將資料儲存到磁碟上。第一種情況一般可以使用某種形式的多執行緒來實現;我們稱這種組織計算的方式為併發或互動。後一種情況一般可以用單執行緒應用程式來實現,在本文中,它被稱為序列執行。


有關併發應用程式的設計是一個非常複雜的過程。一般非常有錢的(who make a ton of money)人比較喜歡它,因為要計算出一個給定的任務採用併發執行到底有多大的好處,通常需要多年的研究。本文並不想要教您如何設計多執行緒應用程式。相反,我要向您指出某些多執行緒應用程式設計的問題所在,而且,我使用真實(real-life)的效能測試來討論我的例子。在閱讀過本文後,您應該能夠觀察一個給定的設計,並且能夠決定某種設計是否提高了該應用程式的整體效能。


多執行緒應用程式設計步驟中的一部分工作,就是要決定在何處存在可能潛在地引起資料毀壞的多執行緒資料訪問衝突,以及如何使用執行緒的同步來避免這種衝突。這項任務(以後,本文將稱之為執行緒編序(thread serialization))是許多有關多執行緒的文章的主題,(例如,MSDN Library中的 "Synchronization on the Fly"或"Compound Win32 Synchronization s"),在本文中將絲毫不涉及對它的討論。有關在本文中要討論的,我們將假定需要併發的計算並不共享任何資料,並且因此而不需要任何執行緒編序。這種約定看起來可能有點苛刻,但是請您牢記,不可能有關於同步多執行緒應用程式的“通用”的討論,因為每一次編序都將強加一個唯一的“等待-醒來”結構(waiting-and-waking pattern)到已編序的執行緒,它將直接地影響效能。


Win32下的大多數輸入/輸出(I/O)操作有兩種形態:同步或非同步。已經被證明在許多的情況下,一個使用同步I/O的多執行緒設計可以被使用非同步單執行緒I/O的設計來模擬。本文並不討論作為多執行緒替代形式的非同步單執行緒I/O,但是,我建議您最好兩種設計都考慮。

  注意到Win32 I/O系統設計的方式是提供一些機制,使得非同步I/O要優於同步I/O(例如,I/O全能埠(completion ports))。我計劃在以後的文章中討論有關同步I/O和非同步I/O的問題。


正如在"Multiple Threads in the User Interface"一文中所指出的,多執行緒和圖形使用者介面(GUI)不能很好地共同工作。在本文中,我假設後臺執行緒可以執行其工作而根本不需要使用Windows GUI;我所處理的這種型別的執行緒僅僅是“工作執行緒”,它僅在後臺執行計算,而不需要與使用者的直接互動。


有有限計算,同樣也有與之相對應的無限計算。端應用程式中的一個“傾聽”執行緒就是無限計算的一個例子,它沒有任何的目的,只是等待一個客戶連線到伺服器。在一個客戶已經連線之後,該執行緒就傳送一個通知到主執行緒,並且返回到“傾聽”狀態,直到下一個客戶的連線。很自然,這樣的一種計算不可能駐留在同一個作為應用程式使用者介面(UI)的執行緒之中,除非使用一種非同步I/O操作。(請注意,這個特定的問題能夠,也應該透過使用非同步I/O和全能(completion)埠來解決,而不是使用多執行緒,我在這裡使用這個例子僅僅是用作演示)。在本文中,我將只考慮有限計算,就是說,應用程式的子任務將在有限的時間段之後結束。


基於的計算和基於I/O的計算


  對於一個單個的執行緒,決定所給定的計算是否是一個優秀的方案的最重要因素是,該計算是一個基於CPU的計算還是基於I/O的計算。基於CPU的計算是指這種計算的大多數時間CPU都非常“忙”。典型的基於CPU的計算如下:



複雜的數學計算,例如複數的計算、圖形的處理、或螢幕後臺圖形計算


對駐留在中的影像的操作,例如在一個文字檔案的記憶體映象中的給定字串。


  相比較而言,基於I/O的計算是這樣的一種計算,它的大多數時間要花費在等待I/O請求的結束。在大多數的作業系統中,正在進入的裝置I/O將被非同步地處理,可能是由一個專門的I/O處理器來處理,或由一個有的中斷處理程式來處理,並且,來自於某個應用程式的I/O請求將會掛起呼叫執行緒,直到I/O結束。一般來說,花費大部分時間來等待I/O請求的執行緒不會與其他的執行緒爭奪CPU時間;因此,同基於CPU的執行緒相比,基於I/O的計算可能不會降低其他執行緒的效能,(稍後,我將解釋這一論點)


  但是請注意,這種比較是非常理論性的。大多數的計算都不是純粹的基於I/O的或純粹的基於CPU的,而是基於I/O的計算和基於CPU的計算都包含。同一集合的計算可能在一種方案中使用順序計算而執行良好,而在另一種方案中使用併發的計算,這取決於基於CPU的計算和基於I/O的計算的相對劃分。


多執行緒設計的目標


  在想要對您的應用程式應用多執行緒之前,您應該問問自己這種轉變的目標是什麼。多執行緒有許多潛在的優點:



增強的效能


增強的容量(throughput)


更好地使用者響應(responsiveness)


  讓我們依次討論上面的每一個優點。


效能


  考慮到時間,讓我們簡單地定義“效能”就是給定的一個或一組計算所消耗的全部時間。按照其定義,則效能的比較就僅僅是對有限計算而言的。


  無論您相信與否,多執行緒方案對應用程式的效能的提高是非常有限的。這裡面的原因不是很明顯,但是它非常有道理:



除非是該應用程式執行於一個多處理器的機器上,(在這種情況下,子計算真正地是並行執行的),基於CPU的計算在多執行緒情況下不可能比在單執行緒情況下的執行速度快。這是因為,無論計算被分解成小塊(在多執行緒的情況下)或大塊(在同一執行緒中計算按順序挨個執行的情況下),只有一個CPU,而且它必需執行所有的計算。結果是,對於一組給定的計算,如果是以多個執行緒來執行,那麼一般會比按序列方式計算完成的時間要長,因為它增加了建立執行緒和線上程之間切換CPU的額外負擔。


一般來說,必定會有某些情況,無論多個計算的完成誰先誰後,但是它們的結果必需同步。例如,使用多個執行緒來併發的讀多個檔案到記憶體中,那麼檔案被處理的順序我們是不關心的,但是必需等到所有的資料都讀入記憶體之後,應用程式才能開始處理。我們將在“容量”一節討論這個想法。

  在本文中,我們將以消耗的時間,即完成所有的計算所消耗的總的時間,來衡量效能。


容量(Throughput)


  容量(或響應),是指每一個計算的平均處理週期(turnaround)的時間。為了演示容量,讓我們假設一個超級市場的例子(它總是一個有關作業系統的極好的演示工具):假設每一個計算就是一個在結算櫃檯被服務的顧客。對於超級市場來說,既可以為每一個顧客開設一個結算櫃檯,也可以把所有的顧客集中起來透過一個結算櫃檯。為了我們分析的需要,假設是有多個結算櫃檯的情況,但是,僅有一個收銀員(可憐的傢伙!)來服務所有的顧客,而不考慮顧客是在一個櫃檯前排隊或多個櫃檯前排隊。這個超級收銀員將高速地從一個櫃檯跳到下一個櫃檯,一次僅處理(ringing up)一個顧客的一件商品,然後,就移動到下一個顧客。這個超級的收銀員就象是被多個計算所割裂的CPU。

  就象我們在前面的“效能”一節中所看到的,服務所有顧客的總的時間並沒有因為有多個結算櫃檯開啟而減少,因為無論顧客是在一個櫃檯還是多個櫃檯被服務,總是這一個收銀員來完成所有的工作。但是,事情是這樣,同只有一個結算櫃檯相比,顧客還是喜歡這種超級收銀員的方式。這是因為一般情況下,顧客的手推車裡的商品數的差別是巨大的,某些顧客的手推車中有一大堆的商品,而某些顧客則只想買很少幾件商品。如果您曾經只希望買一盒 granola bars和一夸脫牛奶,而卻排在某個來為全家24口人採購的先生後面,那您就知道我說的是意味著什麼了。

  無論怎樣,如果您能夠被 Clark Kent 先生以高速度服務,而不是在那裡排隊,您就不會太在意完成結帳的時間是否稍長,因為不管怎麼樣,兩件商品都會很快地被處理完。而滿載著為24口人採購的商品的手推車是在另一個櫃檯被處理的,所以您可以很快就完成結帳而離開。

  因此,容量就是度量在一個給定的時間內有多少個計算可以被執行。每一個計算是這樣度量它的程式的,那就是要比較以下的兩個時間:完成本計算花費了多少的時間,以及假設該計算被首先處理的話要花費多少時間。換句話說,如果您去了超級市場,並且希望兩分鐘就離開那裡,但是實際上您花費了兩個小時來為您的兩件商品結算,原因是您排在了購買其1997生產線的 Betty Crocker 的後面,那麼不得不說,您的程式非常失敗。

  在本文中,我們這樣定義一個計算的響應時間,計算完成所消耗的時間除以預計要消耗的時間。那麼,如果一個應該消耗 10 毫秒(ms)的計算,而實際上消耗了 20 ms,那麼它的響應處理週期就是 2,但是,如果就是同一個計算,卻消耗了 200 ms (可能是因為有另一個長的計算與之競爭並優先)才結束,那麼響應處理週期就是 20。顯然,響應處理週期是越短越好。

  我們在後面將會看到,在將多執行緒引入一個應用程式中時,即使導致了整體效能的下降,容量仍然可能是一個有實際意義的因素;但是,要使容量成為一個有實際意義的因素,必需滿足下面的一些條件:



每一個計算必需是相互獨立的,只要計算一結束,任何計算的結果都可以被處理或使用。如果您是某個大學足球隊的隊員,而且您們每一個隊員都在同一個超級市場買自己的旅行食品,那麼您的商品是先被處理還是後被處理、您花費了多長的時間為兩件商品結帳、以及您為此等待了多長的時間,這些都無關緊要,因為最後您的汽車是不會離開的,除非所有的隊員都買完了食品。所不同的只是您的等待時間,要麼是花費在排隊等待結帳,要麼是如果超級收銀員已經為您服務,時間就花費在等待其他人上。

  這一點很重要,但卻常被忽略。就象我前面所提到的,大多數的應用程式遲早都會顯式或隱式地同步其計算。例如,如果您的應用程式從不同的檔案併發地收集資料,您可能會想要在螢幕上顯示結果,或者把它們儲存到另一個檔案中。在前面一種情況下(在螢幕上顯示結果),您應該意識到,大多數圖形系統都執行某種的內部批處理或序列操作,除非所有的輸出資料都已收集到,否則是根本不會有好的顯示的;在後面的情況下,(儲存結果到另一個檔案),除非整個原型檔案已被寫入完畢,一般不是您的應用程式(或其他的應用程式)所能完全處理的。所以,如果某個人或某些東西以某種形式將結果順序化了,不管是應用程式、作業系統、甚至是使用者,那麼您在處理檔案時所能得到的好處可能就會消失了。


計算之間在量上必需有明顯的差異。如果超級市場中的每一個顧客都只有兩件商品需要結帳,則超級收銀員方式一點優勢都沒有;如果他不得不在3個結算櫃檯之間跳來跳去,而每一個要被服務的顧客僅有2個(或3個、4個或n個)商品要結算,那麼每一個顧客都不得不等待幾倍的時間來完成他或她的結算,這比讓所有的顧客在一起排隊還要糟糕。在這裡把多執行緒想象為shock吸收裝置:短的計算並不會冒被排在長的計算之後的危險,但是它們被分成執行緒並且花費了更多的時間,而本來它們可以在更短的時間內完成。


如果計算的長短可以事先決定,那麼序列處理可能比多執行緒處理要好,您只要簡單地以時間長短按升序排列計算就可以了。在超級市場的例子中,就相當於按照顧客的商品數來站排(Express Lane 方案的一種變種),這種想法是基於這樣的考慮,只有很少的商品的顧客很喜歡它,因為他們不會為一點的事情而耽誤很多的時間, 而那些有很多貨物的顧客也不會在意,因為無論如何要完成其所有的結算都要花費很長的時間,而且在他們前面的每一個人的商品都比它少。

  如果只是大致知道計算時間的一個範圍,但是您的應用程式不能排序這些計算,那麼您應該花些時間做一次最壞情況的分析。在這樣的分析中,您應該假定這些計算不是按照時間的升序順序來排序的,相反,它們是按照時間的降序來排序的。從響應這個角度來講,這中方案是最壞的情形,因為按照前面所定義的公式,每一個計算都將具有其最高可能的響應處理週期。


快速響應(Responsiveness)


  我將在這裡討論的、應用程式多執行緒化的最後一個準則是快速響應(在語言上與響應非常接近,足以使您迷惑不解)。在本文中,如果一個應用程式的設計是保證使用者總是能夠在一個很短的時間(很短的時間指時間非常短,使得使用者感覺不到應用程式被掛起)內完成與應用程式的互動,那麼我們就簡單一點,定義該應用程式為響應快速的應用程式。

  對於一個帶有 GUI 的 Win32 應用程式,快速響應可以被很簡單地實現,只要確保長的計算被委託給後臺執行緒,但是實現快速響應所要求的結構可能要求較高的技巧,正如我前面所提到的,某些人可能會等待某個計算在某個時間返回,所以在後臺執行一個長的計算可能需要改變使用者介面(例如,需要新增一個“取消”按鈕,並且依賴該計算結果的選單項也不得不變灰)。

  除了效能、容量和快速響應之外,其他的一些原因也可能影響多執行緒設計。例如,在某些結構下,必需讓計算以一種偽隨機方式(腦海中再次出現的例子是Bolzmann 機器型別的神經,在這種網路中,僅當該網路中的每一個節點非同步執行其計算時,該網際網路絡的預期行為才能夠工作)。但是,在本文中,我將把討論的範圍限制在上面所涉及的三個因素,那就是:效能、容量和快速響應。


測試的實現


  我曾經聽說過許多關於抽象(abstraction)機制的討論,說它封裝了所有多執行緒的糟糕(nasty)方面到一個 C++ 中,並且因此使一個應用程式獲得了多執行緒的全部優點,而不是缺點。

  在本文中,我一開始就設計這樣一個抽象。我將為一個 C++ 的類 ConcurrentExecution 定義一個原型,該類將含有成員例如:DoConcurrent 和 DoSerial,並且這兩個成員函式的引數將是一個普通物件陣列和一個回撥函式陣列,這些回撥函式將被每一個物件併發或序列地呼叫。該 C++ 類將封裝所有關於保持該執行緒和內部資料結構的真實(gory)細節。

  但是,對我來說,從一開始我就十分清楚,這樣的一個抽象的用處十分有限,因為在設計一個多執行緒應用程式時的最大量的工作成了一個無法自動完成的任務,這個工作就是決定如何實現多執行緒。ConcurrentExecution 的第一個限制是回撥函式將不允許顯式或隱式的共享資料;或回撥函式需要任何其他形式的同步操作,而這些同步操作將立刻犧牲掉所有該抽象所帶來的優點,並且開啟所有“精彩”的同步世界中的陷阱和圈套,例如死鎖、競爭衝突、或需要非常複雜的複合同步物件。

  同樣,也不允許那些可能潛在地被併發執行的計算來呼叫 UI,因為就象我前面所講到的,Win32 API 對於呼叫 UI 的執行緒強迫了許多個隱式的同步操作。請注意,還有許多其他的 API 子集和庫對於共享它們的執行緒強迫了隱式的同步操作。

  這些的限制使 ConcurrentExecution 只具有極其有限的功能,說具體一點,就是一個管理純粹工作者執行緒的抽象(完全獨立的計算大多數情況下僅限於在非連續記憶體區域的數學計算)。

  然而,事實證明實現 ConcurrentExecution 類並且在效能測試中使用它是非常有用的,因為,當我實現了該類,並且設計和執行了該測試之時,許多關於多執行緒的隱藏起來的細節都暴露出來了。請清楚以下一點,雖然 ConcurrentExecution 類可以使多執行緒更容易處理,但是如果想要在商業產品中使用它,那麼該類的實現還需要一些其他的工作。特別要提到的一點時,我忽略了所有的錯誤情況處理,這是不可忍受的。但是我假定只用於測試時(我明顯地使用了 ConcurrentExecution),錯誤不會出現。


ConcurrentExecution 類


下面是 ConcurrentExecution 類的原型:


class ConcurrentExecution

{

< private members omitted>

public:

ConcurrentExecution(int iMaxNumberOfThreads);

~ConcurrentExecution();

int DoForAllObjects(int iNoOfObjects,long *ObjectArray,

CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,

CONCURRENT_FINISHING_ROUTINE pObjectTenated);

BOOL DoSerial(int iNoOfObjects, long *ObjectArray,

CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,

CONCURRENT_FINISHING_ROUTINE pObjectTerminated);

};


該類是從 Thrdlib.dll 庫中匯出的,而 Thrdlib.dll 庫是示例測試套件 THRDPERF 中的一個工程。在討論該類的內部結構之前,讓我們首先討論成員函式的語義(semantics):


ConcurrentExecution::ConcurrentExecution(int iMaxNumberOfThreads)

{

m_iMaxArraySize = min(iMaxNumberOfThreads, MAXIMUM_WAIT_OBJECTS);

m_hThreadArray = (HANDLE *)VirtualAlloc(NULL,m_iMaxArraySize*sizeof(HANDLE),

MEM_COMMIT,PAGE_READWRITE);

m_hObjectArray = (D *)VirtualAlloc(NULL,m_iMaxArraySize*sizeof(DWORD),

MEM_COMMIT,PAGE_READWRITE);

// 當然,一個真正的實現必需在這裡提供對錯誤的處理...

};


您可能會注意到建構函式 ConcurrentExecution 有一個數字引數。該引數指定了該類的例項所支援的“併發的最大度數”;換句話說,如果某個 ConcurrentExecution 的例項被建立時,n 是它的一個引數,那麼在任何給定的時間不能有超過 n 個計算在執行。根據我們以前的分析,該引數就意味“無論有多少個顧客在等待,開啟的結算櫃檯數不要多於 n 個”。


int DoForAllObjects(int iNoOfObjects,long *ObjectArray,

CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,

CONCURRENT_FINISHING_ROUTINE pObjectTerminated);


  這是在這裡被實現的唯一有趣的成員函式。DoForAllObjects 的主要引數是一個物件的陣列、一個處理器函式、和一個終結器函式。關於物件完全沒有強制的格式;每次該處理器被呼叫時,將有一個物件被傳遞給它,而且完全由該處理器來解釋物件。第一個引數 iNoOfObjects,僅僅是要 ConcurrentExecution 知道在物件陣列中的元素數。請注意,在呼叫 DoForAllObjects 時,如果物件陣列的長度為 1,那麼它與呼叫 CreateThread 就非常相似(有一點不同,那就是 CreateThread 不接受一個終結器引數)。

  DoForAllObjects 的語義如下:處理器將為每一個物件而呼叫。物件被處理的順序並未指定;所有能夠擔保的只是每一個物件都將在某個時間被傳遞給處理器。併發的最大度數是由傳遞給 ConcurrentExecution 物件的建構函式的引數來決定的。

  處理器函式不能訪問共享的資料,並且不能呼叫到 UI 或做任何其他需要顯式或隱式地序列操作的事情。目前,僅存在一個處理器函式能夠對所有的物件工作;但是,要使用處理器陣列來替代該處理器引數將是簡單的。


該處理器的原型如下:


typedef DWORD (WINAPI *CONCURRENT_EXECUTION_ROUTINE)

(LPVOID lpParameterBlock);


  當該處理器已經完成了在一個物件上的工作之後,終結器函式將立即被呼叫。與處理器不同,終結器函式是在該呼叫函式的環境中被序列呼叫的,並且可以呼叫所有的例程和訪問呼叫程式所能夠訪問的所有資料。但是,應該要注意的是,終結器應該被儘可能地,因為終結器中的長計算會影響 DoForAllObjects 的效能。請注意,儘管只要處理器結束了每一個物件終結器就會立即被呼叫,直到最後一個物件已經被終結之前,DoForAllObjects 本身並沒有返回。

  我們為什麼要經歷這麼多使用終結器的痛苦?我們同樣可以讓每一個計算在處理器函式的最終結束時執行終結器程式碼,是嗎?

這樣基本上是可以的;但是,有必要強調終結器是在呼叫 DoForAllObjects的執行緒環境中被呼叫的。這樣的設計使在每一個計算進入時處理它們的結果更加容易,而無須擔心同步問題。


終結器函式的原型如下:


typedef DWORD (WINAPI *CONCURRENT_FINISHING_ROUTINE)

(LPVOID lpParameterBlock,LPVOID lpResultCode);


  第一個引數是被處理的物件,第二個引數是處理器函式在該物件上的結果。

  DoForAllObjects 的同類是 DoSerial,DoSerial 與 DoForAllObjects 具有相同的引數列表,但是計算是被以序列的順序處理的,並且以列表中的第一個物件開始。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-996882/,如需轉載,請註明出處,否則將追究法律責任。

相關文章