Win32多執行緒

Win32多執行緒是Microsoft Developer Network 技術小組研發的多執行緒應用程式的策略。

基本介紹

  • 中文名:Win32多執行緒
  • 外文名:Windows multithreads
  • 作者:Microsoft公司供稿
  • 技術小組:Microsoft Developer Network
  • 摘要:多執行緒應用程式的策略
Win32 多執行緒的性能:,摘要,介紹,多執行緒辭彙,基於CPU的計算和基於I/O的計算,多執行緒設計的目標,容量(Throughput),ConcurrentExecution 類,

Win32 多執行緒的性能:

作者:Microsoft公司供稿
Ruediger R. Asche

摘要

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

介紹

在您所能夠找到的有關多執行緒的資料中,多數都是講述同步概念的。例如,如何串列化(serialize)共享公共數據的執行緒。這種把重點放在討論同步上是有意義的,因為同步是多執行緒編程中不可缺少的一部分。本文則後退了一步(takes a step back),主要討論有關多執行緒中很少有人涉及的一面:決定一個計算如何能夠被有意義地拆分為多個執行緒。本文中所使用的示例程式,THRDPERF,在Microsoft? Windows? 95和Windows NT? 兩個平台之上,針對同一個計算採取串列和並發兩種方法分別實現了測試套件(test suite),並從吞吐量和性能兩方面來比較它們。
本文的第一部分建立了一些有關多執行緒應用程式的辭彙(vocabulary),討論測試套件的範圍,並且介紹了示例程式套件是如何設計的。第二部分討論測試的結果,並且包括對於多執行緒應用程式設計的建議。與之相關的文章 "Interacting with Microsoft Excel: A Case Study in OLE Automation" 討論有關該示例程式套件的一個有趣的問題,即使用測試集合所獲得的數據是如何使用 OLE Automation被輸入 Microsoft Excel 中的。
如果您是經驗豐富的多執行緒應用程式編程者,您可以跳過介紹部分,而直接到下面的“結果”部分。

多執行緒辭彙

很長一段時間以來,您的應用程式一直被使用——它運轉出色,是可以信賴的,而且 the whole bit——但它十分的遲緩,並且您有如何利用多執行緒的想法。但是,在開始這樣做之前請稍等一會兒,因為這裡有許多的陷阱,它們使您相信某種多執行緒設計是非常完美的,但實際上並不是這樣。
在您跳至有關要進入的結論之前,首先讓我們澄清一下在本文中將不討論的內容:
在 Microsoft Win32? 應用程式編程接口(API)下提供多執行緒訪問的庫是不同的,但是我們不關注這一問題。示例程式套件,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 Objects"),在本文中將絲毫不涉及對它的討論。有關在本文中要討論的,我們將假定需要並發的計算並不共享任何數據,並且因此而不需要任何執行緒編序。這種約定看起來可能有點苛刻,但是請您牢記,不可能有關於同步多執行緒應用程式的“通用”的討論,因為每一次編序都將強加一個唯一的“等待-醒來”結構(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)連線埠來解決,而不是使用多執行緒,我在這裡使用這個例子僅僅是用作演示)。在本文中,我將只考慮有限計算,就是說,應用程式的子任務將在有限的時間段之後結束。

基於CPU的計算和基於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 pObjectTerminated);
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 = (DWORD *)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 具有相同的參數列表,但是計算是被以串列的順序處理的,並且以列表中的第一個對象開始。
請注意:本節的討論是非常技術性的,所以假設您理解很多有關 Win32 執行緒 API 的知識。如果您對如何使用 ConcurrentExecution 類來收集測試數據更加感興趣,而不是對 ConcurrentExecution::DoForAllObjects 是如何被實現的感興趣,那么您現在就可以跳到下面的“使用 ConcurrentExecution 來試驗執行緒性能”一節。
讓我們從 DoSerial 開始,因為它很大程度上是一個“不費腦筋的傢伙”:
BOOL ConcurrentExecution::DoSerial(int iNoOfObjects,long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pProcessor,
CONCURRENT_FINISHING_ROUTINE pTerminator)
{
for (int iLoop=0;iLoop<iNoOfObjects;iLoop++)
{
pTerminator((LPVOID)ObjectArray[iLoop],(LPVOID)pProcessor((LPVOID)ObjectArray[iLoop]));
};
return TRUE;
};
這段代碼只是循環遍歷該數組,在每一次疊代中調用處理器,然後在處理器和對象本身的結果上調用終結器。幹得既乾淨又漂亮,不是嗎?
令人感興趣的成員函式是 DoForAllObjects。乍一看,DoForAllObjects 所要做的也沒有什麼特別的——請求作業系統創建為每一個計算一個執行緒,並且確保終結器函式能夠被正確地調用。但是,有兩個問題使得 DoForAllObjects 比它的表面現象要複雜:第一,當計算的數目多於可用的執行緒數時,ConcurrentExecution 的一個實例所創建的“並發的最大度數”參數可能需要一些附加的記錄(bookkeeping)。第二,每一個計算的終結器函式都是在調用 DoForAllObjects 的執行緒的上下文中被調用的,而不是在該計算運行所處的執行緒上下文中被調用的;並且,終結器是在處理器結束之後立刻被調用的。要處理這些問題還是需要很多技巧的。
讓我們深入到代碼中,看看究竟是怎么樣的。該段代碼是從檔案 Thrdlib.cpp 中繼承來的,但是為了清除起見,已經被精簡了:
int ConcurrentExecution::DoForAllObjects(int iNoOfObjects,long *ObjectArray,
CONCURRENT_EXECUTION_ROUTINE pObjectProcessor,
CONCURRENT_FINISHING_ROUTINE
pObjectTerminated)
{
int iLoop,iEndLoop;
DWORD iThread;
DWORD iArrayIndex;
DWORD dwReturnCode;
DWORD iCurrentArrayLength=0;
BOOL bWeFreedSomething;
char szBuf[70];
m_iCurrentNumberOfThreads=iNoOfObjects;
HANDLE *hPnt=(HANDLE *)VirtualAlloc(NULL,m_iCurrentNumberOfThreads*sizeof(HANDLE)
,MEM_COMMIT,PAGE_READWRITE);
for(iLoop=0;iLoop<m_iCurrentNumberOfThreads;iLoop++)
hPnt[iLoop] = CreateThread(NULL,0,pObjectProcessor,(LPVOID)ObjectArray[iLoop],
CREATE_SUSPENDED,(LPDWORD)&iThread);
首先,我們為每一個對象創建單獨的執行緒。因為我們使用 CREATE_SUSPENDED 來創建該執行緒,所以還沒有執行緒被啟動。另一種方法是在需要時創建每一個執行緒。我決定不使用這種替代的策略,因為我發現當在一個同時運行了多個執行緒的應用程式中調用時, CreateThread 調用是非常浪費的;這樣,同在運行時創建每一個執行緒相比,在此時創建執行緒的開銷將更加容易接受,
for (iLoop = 0; iLoop < m_iCurrentNumberOfThreads; iLoop++)
{
HANDLE hNewThread;
bWeFreedSomething=FALSE;
// 如果數組為空,分配一個 slot 和 boogie。
if (!iCurrentArrayLength)
{
iArrayIndex = 0;
iCurrentArrayLength=1;
}
else
{
// 首先,檢查我們是否可以重複使用任何的 slot。我們希望在查找一個新的 slot 之前首先// 做這項工作,這樣我們就可以立刻調用該就執行緒的終結器...
iArrayIndex=WaitForMultipleObjects(iCurrentArrayLength,
m_hThreadArray,FALSE,0);
if (iArrayIndex==WAIT_TIMEOUT) // no slot free...
{
{
if (iCurrentArrayLength >= m_iMaxArraySize)
{
iArrayIndex= WaitForMultipleObjects(iCurrentArrayLength,
m_hThreadArray,FALSE,INFINITE);
bWeFreedSomething=TRUE;
}
else // 我們可以釋放某處的一個 slot,現在就這么做...
{
iCurrentArrayLength++;
iArrayIndex=iCurrentArrayLength-1;
}; // Else iArrayIndex points to a thread that has been nuked
};
}
else bWeFreedSomething = TRUE;
}; // 在這裡,iArrayIndex 包含一個有效的索引以存儲新的執行緒。
hNewThread = hPnt[iLoop];
ResumeThread(hNewThread);
if (bWeFreedSomething)
{
GetExitCodeThread(m_hThreadArray[iArrayIndex],&dwReturnCode); //錯誤
CloseHandle(m_hThreadArray[iArrayIndex]);
pObjectTerminated((void *)m_hObjectArray[iArrayIndex],(void *)dwReturnCode);
};
m_hThreadArray[iArrayIndex] = hNewThread;
m_hObjectArray[iArrayIndex] = ObjectArray[iLoop];
}; // 循環結束
DoForAllObjects 的核心是 hPnt,它是一個對象數組,這些對象是當 ConcurrentExecution 對象被構造時分配的。該數組能夠容納最大數目的執行緒,此最大數目與在構造函式中指定的最大並發度數相對應;因此,該數組中的每一個元素都是一個"slot",並有一個計算居於之中。
關於決定如何填充和釋放的 slots 算法如下:該對象數組是從頭到尾遍歷的,並且對於每一個對象,我們都做如下的事情:如果尚未有 slot 已經被填充,我們使用當前的對象來填充該數組中的第一個 slot,並且繼續執行將要處理當前對象的執行緒。如果至少有一個 slot 被使用,我們使用 WaitForMultipleObjects 函式來決定是否有正在運行的任何計算已經結束;如果是,我們在該對象上調用終結器,並且為新對象“重用”該 slot。請注意,我們也可以首先填充每一個空閒的 slot,直到沒有剩餘的 slots 為止,然後開始填充空的 slot。但是,如果我們這樣做了,那么空出 slot 的終結器函式將不會被調用,直到所有的 slot 都已經被填充,這樣就違反了我們有關當處理器結束一個對象時,終結器立刻被調用的要求。
最後,還有沒有空閒 slot 的情況(就是說,當前激活的執行緒數等於 ConcurrentExecution 對象所允許的最大並發度數)。在這種情況下,WaitForMultipleObjects 將被再次調用以使得 DoForAllObjects 處於“睡眠”狀態,直到有一個 slot 空出;只要這種情況一發生,終結器就被在空出 slot 的對象上調用,並且工作於當前對象的執行緒被繼續執行。
終於,所有的計算要么都已經結束,要么將占有對象數組中的 slot。下列的代碼將會處理所有剩餘的執行緒:
iEndLoop = iCurrentArrayLength;
for (iLoop=iEndLoop;iLoop>0;iLoop--)
{
iArrayIndex=WaitForMultipleObjects(iLoop, m_hThreadArray,FALSE,INFINITE);
if (iArrayIndex==WAIT_FAILED)
{
GetLastError();
_asm int 3; // 這裡要做一些聰明的事...
};
GetExitCodeThread(m_hThreadArray[iArrayIndex],&dwReturnCode); // 錯誤?
if (!CloseHandle(m_hThreadArray[iArrayIndex]))
MessageBox(GetFocus(),"Can't delete thread!","",MB_OK); // 使它更好...
pObjectTerminated((void *)m_hObjectArray[iArrayIndex],
(void *)dwReturnCode);
if (iArrayIndex==iLoop-1) continue; // 這裡很好,沒有需要向後填充
m_hThreadArray[iArrayIndex]=m_hThreadArray[iLoop-1];
m_hObjectArray[iArrayIndex]=m_hObjectArray[iLoop-1];
};
最後,清除:
if (hPnt) VirtualFree(hPnt,m_iCurrentNumberOfThreads*sizeof(HANDLE),
MEM_DECOMMIT);
return iCurrentArrayLength;
};
使用 ConcurrentExecution 來試驗執行緒性能
性能測試的範圍如下:測試應用程式 Threadlibtest.exe 的用戶可以指定是否測試基於 CPU 的或基於 I/O 的計算、執行多少個計算、計算的時間有多長、計算是如何排序的(為了測試最糟的情況與隨機延遲),以及計算是被並發執行還是串列執行。
為了消除意外的結果,每一個測試可以被執行十次,然後將十次的結果拿來平均,以產生一個更加可信的結果。
通過選擇選單選項 "Run entire test set",用戶可以請求運行所有測試變數的變形。在測試中使用的計算長度在基礎值 10 和 3,500 ms 之間變動(我一會兒將討論這一問題),計算的數目在 2 和 20 之間變化。如果在運行該測試的計算機上安裝了 Microsoft Excel,Threadlibtest.exe 將會把結果轉儲在一個 Microsoft Excel 工作表,該工作表位於 C:\Temp\Values.xls。在任何情況下結果值也將會被保存到一個純文本檔案中,該檔案位於 C:\Temp\Results.fil。請注意,我對於協定檔案的位置使用了硬編碼的方式,純粹是懶惰行為;如果您需要在您的計算機上重新生成測試結果,並且需要指定一個不同的位置,那么只需要重新編譯生成該工程,改變檔案 Threadlibtestview.cpp 的開頭部分的 TEXTFILELOC 和 SHEETFILELOC 標識符的值即可。
請牢記,運行整個的測試程式將總是以最糟的情況來排序計算(就是說,執行的順序是串列的,最長的計算將被首先執行,其後跟隨著第二長的計算,然後以次類推)。這種方案犧牲了串列執行的靈活性,因為並發執行的回響時間在一個非最糟的方案下也沒有改變,而該串列執行的回響時間是有可能提高的。
正如我前面所提到的,在一個實際的方案中,您應該分析每一個計算的時間是否是可以預測的。
使用 ConcurrentExecution 類來收集性能數據的代碼位於 Threadlibtestview.cpp 中。示例應用程式本身 (Threadlibtest.exe) 是一個真正的單文檔界面 (SDI) 的 MFC 應用程式。所有與示例有關的代碼都駐留在 view 類的實現 CThreadLibTestView 中,它是從 CEasyOutputView 繼承而來的。(有關對該類的討論,請參考"Windows NT Security in Theory and Practice"。)這裡並不包含該類中所有的有趣代碼,所包含的大部分是其數字統計部分和用戶界面處理部分。執行測試中的 "meat" 在 CThreadLibTestView::ExecuteTest 中,將執行一個測試運行周期。下面是有關 CThreadLibTestView::ExecuteTest 的簡略代碼:
void CThreadlibtestView::ExecuteTest()
{
ConcurrentExecution *ce;
bCPUBound=((m_iCompType&CT_IOBOUND)==0); // 全局...
ce = new ConcurrentExecution(25);
if (!QueryPerformanceCounter(&m_liOldVal)) return; // 獲得當前時間。
if (!m_iCompType&CT_IOBOUND) timeBeginPeriod(1);
if (m_iCompType&CT_CONCURRENT)
m_iThreadsUsed=ce->DoForAllObjects(m_iNumberOfThreads,
(long *)m_iNumbers,
(CONCURRENT_EXECUTION_ROUTINE)pProcessor,
(CONCURRENT_FINISHING_ROUTINE)pTerminator);
else
ce->DoSerial(m_iNumberOfThreads,
(long *)m_iNumbers,
(CONCURRENT_EXECUTION_ROUTINE)pProcessor,
(CONCURRENT_FINISHING_ROUTINE)pTerminator);
if (!m_iCompType&CT_IOBOUND) timeEndPeriod(1);
delete(ce);
< 其他的代碼在一個數組中排序結果,以供 Excel 處理...>
}
該段代碼首先創建一個 ConcurrentExecution 類的對象,然後,取樣當前時間,(用於統計計算所消耗的時間和回響時間),並且,根據所請求的是串列執行還是並發執行,分別調用 ConcurrentExecution 對象 DoSerial 或 DoForAllObjects 成員。請注意,對於當前的執行我請求最大並發度數為 25;如果您想要運行有多於 25 個計算的測試程式,那么您應該提高該值,使它大於或等於運行您的測試程式所需要的最大並發數
讓我們看一下處理器和終結器,以得到精確測量的結果:
extern "C"
{
long WINAPI pProcessor(long iArg)
{
PTHREADBLOCKSTRUCT ptArg=(PTHREADBLOCKSTRUCT)iArg;
BOOL bResult=TRUE;
int iDelay=(ptArg->iDelay);
if (bCPUBound)
{
int iLoopCount;
iLoopCount=(int)(((float)iDelay/1000.0)*ptArg->tbOutputTarget->m_iBiasFactor);
QueryPerformanceCounter(&ptArg->liStart);
for (int iCounter=0; iCounter<iLoopCount; iCounter++);
}
else
{
QueryPerformanceCounter(&ptArg->liStart);
Sleep(ptArg->iDelay);
};
return bResult;
}
long WINAPI pTerminator(long iArg, long iReturnCode)
{
PTHREADBLOCKSTRUCT ptArg=(PTHREADBLOCKSTRUCT)iArg;
QueryPerformanceCounter(&ptArg->liFinish);
ptArg->iEndOrder=iEndIndex++;
return(0);
}
}
處理器模擬一個計算,其長度已經被放到一個與計算有關的數據結構 THREADBLOCKSTRUCT 中。THREADBLOCKSTRUCT 保持著與計算有關的數據,如其延遲和終止時間(以性能計數“滴答”來衡量),以及反向指針,指向實用化該結構的視圖(view)。
通過簡單的使計算“睡眠”指定的時間就可以模擬基於I/O的計算。基於 CPU的計算將進入一個空的 for 循環。這裡的一些注釋是為了幫助理解代碼的功能:計算是基於 CPU 的,並且假定其執行時間為指定的毫秒數。在本測試程式的早期版本中,我僅僅是要 for 循環執行足夠多的次數以滿足指定的延遲的需求,而不考慮數字的實際含義。(根據相關的代碼,對於基於I/O的計算該數字實際意味著毫秒,而對於基於CPU的計算,該數字則意味著疊代次數。)但是,為了能夠使用絕對時間來比較基於CPU的計算和基於I/O的計算,我決定重寫這段代碼,這樣無論對於基於CPU的計算還是基於I/O的計算,與計算有關的延遲都是以毫秒測量。
我發現對於具有指定的、預先定義時間長度的基於CPU的計算,要編寫代碼來模擬它並不是一件簡單的事情。原因是這樣的代碼本身不能查詢系統時間,因為所引發的調用遲早都會交出 CPU,而這違背了基於 CPU 的計算的要求。試圖使用異步多媒體時鐘事件同樣沒有得到滿意的效果,原因是 Windows NT 下計時器服務的工作方式。設定了一個多媒體計時器的執行緒實際上被掛起,直到該計時器回調被調用;因此,基於 CPU 的計算突然變成了基於 I/O 的操作了。
於是,最後我使用了一個有點兒低劣的技巧:CThreadLibTestView::OnCreate 中的代碼執行 100 次循環從 1 到 100,000 計數,並且取樣通過該循環所需要的平均時間。結果被保存在成員變數 m_iBiasFactor 中,該成員變數是一個浮點數,它在處理器函式中被使用來決定毫秒如何被“翻譯”成疊代次數。不幸的是,因為作業系統的高度戲劇性的天性,要決定實際上運行一個指定長度的計算要疊代多少次給定的循環是困難的。但是,我發現該策略在決定基於CPU的操作的計算時間方面,完成了非常可信的工作。
注意 如果您重新編譯生成該測試應用程式,請小心使用最最佳化選項。如果您指定了 "Minimize execution time" 最佳化,則編譯程式將檢測具有空的主體的 for 循環,並且刪除這些循環。
終結器非常簡單:當前時間被取樣並保存在計算的 THREADBLOCKSTRUCT 中。在測試結束之後,該代碼計算執行 ExecuteTest 的時間和終結器為每一個計算所調用的時間之間的差異。然後,所有計算所消耗的時間由所有已完成的計算中最後一個計算完成時所消耗的時間所決定,而回響時間則是每一個計算的回響時間的平均值,這裡,每一個回響時間,同樣,定義為從測試開始該執行緒消耗的時間除以該執行緒的延遲因子。請注意,終結器在主執行緒上下文中串列化的運行,所以在共享的 iEndIndex 變數上的遞增指令是安全的。
這些實際就是本測試的全部;其餘的部分則主要是為測試的運行設定一些參數,以及對結果執行一些數學計算。填充結果到 Microsoft Excel 工作單中的相關邏輯將在"Interacting with Microsoft Excel: A Case Study in OLE Automation."一文中討論。
結果
如果您想要在您的計算機上重新創建該測試結果,您需要做以下的事情:
如果您需要改變測試參數,例如最大計算數或協定檔案的位置,請編輯 THRDPERF 示例工程中的 Threadlibtestview.cpp,然後重新編譯生成該應用程式。(請注意,要編譯生成該應用程式,您的計算機需要對長檔案名稱的支持。)
請確保檔案 Thrdlib.dll 在一個 Threadlibtest.exe 能夠連結到它的位置。
如果您想要使用 Microsoft Excel 來查看測試的結果,請確定 Microsoft Excel 已經正確地被安裝在運行該測試的計算機上。
從 Windows 95 或 Windows NT 執行 Threadlibtest.exe,並且從“Run performance tests”選單選擇"Run entire test set"。正常情況下,測試運行一次要花費幾個小時才能完成。
在測試結束之後,檢查結果時,既可以使用普通文本協定檔案 C:\Temp\Results.fil ,也可以使用工作單檔案 C:\Temp\Values.xls。請注意,Microsoft Excel 的自動化(automation)邏輯並不自動地為您從原始數據生成圖表,我使用了幾個宏來重新排列該結果,並且為您生成了圖表。我憎恨數字(number crunching),但是我必需稱讚 Microsoft Excel,因為即使象我一樣的工作表妄想狂(spreadsheet-paranoid),也可以提供這樣漂亮的用戶界面,在幾分鐘之內把幾列數據裝入有用的圖表。
我所展現的測試結果是在一個 486/33 MHz 的帶有 16 MB RAM 的系統收集而來的。該計算機同時安裝了 Windows NT (3.51 版) 和 Windows 95;這樣,在兩個作業系統上的不同測試結果就是可比的了,因為它們基於同樣的硬體系統
那么,現在讓我們來解釋這些值。這裡是總結計算結果的圖表;後面有其解釋。該圖表應該這樣來看:每一個圖表的 x 軸有 6 個值(除了有關長計算的消耗時間表,該表僅含有 5 個值,這是因為在我的測試運行時,對於非常長的計算計時器溢出了)。一個值代表多個計算;我以 2、5、8、11、14 和 17 個計算來運行每一個測試。在 Microsoft Excel 結果工作表中,您將會找到對於基於CPU的計算和基於I/O的計算的執行緒的每一種計算數目的結果,延遲(delay bias)分別是 10 ms、30 ms、90 ms、270 ms,、810 ms 和 2430 ms,但是在該圖表中,我僅包括了 10 ms 和 2430 ms 的結果,這樣所有的數字都被簡化,而且更容易理解。
我需要解釋 "delay bias." 的含義,如果一個測試運行的 delay bias 是 n,則每一個計算都有一個倍數 n 作為其計算時間。例如,如果試驗的是 delay bias 為 10 的 5 個計算,則其中一個計算將執行 50 ms,第二個將執行 40 ms,第三個將執行 30 ms,第四個將執行 20 ms,而第五個將執行 10 ms。並且,當這些計算被串列執行時,假定為最糟的情況,所以具有最長延遲的計算首先被執行,其他的計算按降序排列其後。於是,在“理想”情況下(就是說,計算之間沒有重疊),對於基於CPU的計算來說,全部所需的時間將是 50 ms + 40 ms + 30 ms + 20 ms + 10 ms = 150 ms。
對於消耗時間圖表來說, y 軸的值與毫秒對應,對於回響時間圖表來說,y 軸的值與相對(relative turnaround)長度(即,實際執行所花費的毫秒數除以預期的毫秒數)相對應。
基於 CPU 的任務
正如我們前面所提到的,在一個單處理器的計算機中,基於 CPU 的任務的並發執行速度不可能比串列執行速度快,但是我們可以看到,在 Windows NT 下執行緒創建和切換的額外開銷非常小;對於非常短的計算,並發執行僅僅比串列執行慢 10%,而隨著計算長度的增加,這兩個時間就非常接近了。以回響時間來衡量,我們可以發現對於長計算,並發執行相對於串列執行的回響增益可以達到 50%,但是對於短的計算,串列執行實際上比並發執行更加好。
Windows 95 和 Windows NT 之間的比較
如果我們看一看有關長計算的圖表(即,圖2、4、6 和 8),我們可以發現在 Windows 95 和 Windows NT 下其行為是極其類似的。請不要被這樣的事實所迷惑,即好象 Windows 95 處理基於I/O的計算與基於CPU的計算不同於 Windows NT。我把這一結果的原因歸結為這樣一個事實,那就是我用來決定多少個測試循環與 1 毫秒相對應的算法(如前面所述)是非常不精確的;我發現同樣是這個算法,在完全相同的環境下執行多次時,所產生的結果之間的差異最大時可達20%。所以,比較基於 CPU 的計算和基於 I/O 的計算實際上是不公平的。
Windows 95 和 Windows NT 之間不同的一點是當針對短的計算時。如我們從圖1 和5 所看到的,對於並發的基於I/O的短計算,Windows NT 的效果要好得多。我把這一結果得原因歸結為更加有效率得執行緒創建方案。請注意,對於長得計算,串列與並發I/O操作之間的差別消失了,所以這裡我們處理的是固定的、相對比較小的額外開銷。
對於短的計算,以回響時間來衡量(如圖 3 和 7),請注意,在 Windows NT 下,在10個執行緒處有一個斷點,在這裡更多的計算並發執行有更好的效果,而對於 Windows 95 ,則是串列計算有更好的容量。
請注意這些比較都是基於當前版本的作業系統得到的(Windows NT 3.51 版和 Windows 95),如果考慮到作業系統的問題,那么執行緒引擎非常有可能被增強,所以兩個作業系統之間的各自行為的差異有可能消失。但是,有一點很有趣的要注意,短計算一般不必要使用多執行緒,尤其是在 Windows 95 下。
建議
這些結果可以推出下面的建議:決定多執行緒性能的最主要因素是基於 I/O 的計算和基於 CPU 的計算的比例,決定是否採用多執行緒的主要條件是前台的用戶回響。
讓我們假定在您的應用程式中,有多個子計算可以在不同的執行緒中潛在地被執行。為了決定對這些計算使用多執行緒是否有意義,要考慮下面的幾點。
如果用戶界面回響分析決定某些事情應該在第二執行緒中實現,那么,決定將要執行的任務是基於I/O的計算還是基於CPU 的計算就很有意義。基於I/O的計算最好被重新定位到後台執行緒中。(但是,請注意,異步單執行緒的 I/O 處理可能比多執行緒同步I/O要好,這要看問題而定)非常長的基於CPU的執行緒可能從在不同的執行緒中被執行獲益;但是,除非該執行緒的回響非常重要,否則,在同一個後台執行緒中執行所有的基於 CPU 的任務可能比在不同的執行緒中執行它更有意義。請記住在任何的情況下,短計算在並發執行時一般會線上程創建時有非常大的額外開銷。
如果對於基於CPU的計算 — 即每一個計算的結果只要得到了就立刻能套用的計算,回響是最關鍵的,那么,您應該嘗試決定這些計算是否能夠以升序排序,在此種情況下這些計算串列執行的整體性能仍然會比並行執行要好。請注意,有一些計算機的體系結構的設計是為了能夠非常有效率地處理長的計算(例如矩陣操作),那么,在這樣的計算機上對長的計算實行多執行緒化的話,您可能實際上犧牲了這種結構的優勢。
所有的這些分析都假定該應用程式是運行在一個單處理器的計算機上,並且計算之間是相互獨立的。實際上,如果計算之間相互依賴而需要串列設計,串列執行的性能將不受影響(因為串列是隱式的),而並發執行的版本將總是受到不利的影響。
我還建議您基於計算之間相互依賴的程度決定多執行緒的設計。在大多數情況下子計算執行緒化不用說是好的,但是,如果對於拆分您的應用程式為多個可以在不同執行緒處理的子計算的方法有多種選擇,我推薦您使用同步的複雜性作為一個條件。換句話說,如果拆分成多個執行緒而需要非常少和非常直接的同步,那么這種方案就比需要使用大量且複雜的執行緒同步的方案要好。
最後一個請注意是,請牢記執行緒是一種系統資源,不是無代價的;所以,there may be a greater penalty to multithreading than performance hits alone. 作為第一規則(rule of thumb),我建議您在使用多執行緒時要保持理智並且謹慎。在多執行緒確實能夠給您的應用程式設計帶來好處的時候才使用它們,但是,如果串列計算可以達到同樣的效果,就不要使用它們。
總結
運行附加的性能測試套件產生了一些特殊的結果,這些結果提供了許多有關並發應用程式設計的內部邏輯。請注意我的許多假定是非常基本的;我選擇了比較非常長的計算和非常短的計算,我假定計算之間完全獨立,要么完全是基於I/O的計算,要么是完全基於CPU的計算。而絕大多數的現實問題,如果以計算長度和 boundedness 來衡量,都是介於這兩種情況之間的。請把本文中的材料僅僅作為一個起點,它使您更加詳細地分析您的應用程式,以決定是否使用多執行緒。

相關詞條

熱門詞條

聯絡我們