本節書摘來自華章出版社《多核與gpu程式設計:工具、方法及實踐》一書中的第3章,第3.6節, 作 者 multicore and gpu programming: an integrated approach[阿聯酋]傑拉西莫斯·巴拉斯(gerassimos barlas) 著,張雲泉 賈海鵬 李士剛 袁良 等譯, 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
信号量提供一個十分友善的機制,如果合适地應用,可以通過多線程的實作來獲得最大并行性。然而,這隻是“如果”。由于它提供十分細粒度且橫跨多個線程的程式邏輯并發控制機制,是以高效地使用信号量機制是十分困難的。在面向對象程式設計時代,信号量已經變得不如從前流行。
幸運的是,存在另一個稱為<code>monitor</code>的機制。<code>monitor</code>是一個對象或者子產品,可以供多個線程進行安全通路。為了完成這一功能,某個時刻隻允許一個線程執行一個<code>monitor</code>的任意一個公有的方法。為了阻塞一個線程或者通知一個線程繼續執行(建立關鍵區的兩個基礎功能),<code>monitor</code>将提供一個稱為條件變量的機制。
一個條件變量通常隻允許在<code>monitor</code>内部通路,它包含一個隊列并支援兩個操作。
<code>wait</code>操作:如果一個線程在一個條件變量上執行等待操作,它将會被阻塞并被放置在條件變量隊列中。
<code>signal</code>操作:如果線程在一個條件變量上執行通知操作,将會有一個相應隊列中的線程被釋放(即變為可用)。如果隊列為空,則信号被忽略。
等待和通知操作類似信号量的功能,但是存在一些不同。在條件變量上的等待操作導緻線程的無條件阻塞,而信号量上的等待操作依賴其取值。另外,條件變量信号可以被忽略,而信号量的釋放操作總會将其加一。
條件變量總是與一個條件相關聯,亦即指出線程應該阻塞還是繼續執行的一組規則。這也是<code>monitor</code>較之信号量的最大優勢之一。由于任何複雜的條件可以不借助一系列互相交織的信号量擷取輕松處理,就像我們在讀者–寫者問題中看到的一樣。
作為一個簡單的示例,考慮一個執行銀行賬戶的交易處理的<code>monitor</code>類。<code>withdraw</code>和<code>deposit</code>方法的僞碼如下。

為了保證隻有一個線程在<code>accountmonitor</code>執行個體中運作,兩個方法的入口都被一個互斥量鎖定。在出口處被釋放(第19行)互斥量,或者當一個線程将要被阻塞時(例如發現沒有足夠的資金可供取出),并且被放置在條件變量的隊列中(第12行)時,釋放互斥量。當資金被存入賬戶後,阻塞的線程被喚醒。
當線程被喚醒時發現存入的資金仍然不足時怎麼辦?如果有多個被阻塞線程,并且可能至少有一個線程的取款條件可以滿足,但是并不一定是被喚醒的線程,此時該如何
處理?
上面描述的程式邏輯顯然沒有覆寫這種情況。接下來的讨論将會解答這一問題。
1.線程立即運作。由于一個<code>monitor</code>不能存在兩個執行其方法的活動線程,是以這将自動導緻發出喚醒信号的線程挂起。一旦被喚醒的線程推出<code>monitor</code>,線程就将執行。這種方法與<code>hoare monitor</code>規範一緻,<code>hoare</code>是在19世紀70年代最早提出<code>monitor</code>概念的科學家。
2.喚醒線程将處于等待狀态,直到信号發出線程退出<code>monitor</code>。由于這一延遲,直到線程獲得<code>monitor</code>的控制權,其等待的條件才可能被修改。這就需要一次重新檢查,将控制等待的if語句變為一個<code>while</code>語句。這種方法遵循<code>lampson-redell monitor</code>規範,它較之<code>hoare monitor</code>具有一些優勢,包括具有逾時等待能力以及喚醒所有等待條件變量的線程的能力。這些實作都靠等待的線程重新運作時對條件的再次檢查。
為了差別行為的不同之處,信号的通知操作稱為<code>notify</code>,通知條件變量的所有等待線程的操作稱為<code>notify all</code>。
<code>monitor</code>實作的主要部分(包括qt和java中的實作)都遵循<code>lampson-redell</code>規範,因為額外的功能和内部更簡單實作同時存在。使用這種<code>monitor</code>,可以完成銀行賬戶<code>monitor</code>示例,進而解決上述兩個問題。我們将在研究qt中的<code>monitor</code>機制後重寫代碼。
qt提供<code>qwaitcondition</code>類來支援條件變量,它包含以下方法。
wait,強制線程阻塞。為了避免程式員分别加鎖和解鎖控制<code>monitor</code>入口的互斥量操作,互斥量的引用将會被傳遞給自動執行這些操作的方法。
<code>wakeone</code>,喚醒一個線程。
<code>wakeall</code>:喚醒所有阻塞線程。所有線程都将在<code>montior</code>内部順序執行。
qt提供更為友善的類,它簡化了控制入口的互斥量管理:<code>qmutexlocker</code>。這個類的執行個體應該在每個<code>monitor</code>方法的最開始建立,它負責對控制入口的互斥量加鎖,這也是為何向其構造函數傳遞它的一個引用。這一看上去備援的操作的優勢在于<code>monitor</code>方法可以在多個程式位置終止,而不需要顯式的互斥量解鎖操作。當<code>qmutexlocker</code>類的析構函數被調用時(方法終止時),互斥量将被解鎖,減少開銷并且消除潛在的程式設計錯誤。
代碼清單3-18展示了重寫新的銀行賬戶<code>monitor</code>類的方法。
可以簡單區分<code>monitor</code>方法的三個部分,其中兩個是可選的:
1.入口(可選):檢測條件是否滿足線程繼續執行。
2.中間:操作<code>monitor</code>狀态,并且執行需要互斥的任意操作。
3.出口(可選):通知其他線程可以進入<code>monitor</code>或者繼續執行。
請讀者在代碼清單3-18中找出這三個部分的代碼。
由于在關鍵位置包含對應的程式邏輯,是以<code>monitor</code>提供的互斥機制極大地簡化了多線程應用的設計,并且使得其更易于了解和修改。
已經證明<code>monitor</code>和信号量是等價的,所有使用信号量的程式可以轉變為使用monitor的程式,反之亦然。解決方案的複雜性是問題的獨特特性。
根據關鍵區的位置基于<code>monitor</code>的解決方案可以使用兩種可能的模式。一種選擇是将關鍵區放置于<code>monitor</code>内部,另一種選擇是使用<code>monitor</code>擷取和釋放關鍵區的入口。每種方法都有各自的優勢和劣勢,這将在下面幾節進行介紹。
将一個關鍵區放入<code>monitor</code>内部是一種完美的方案,由于每個時刻隻允許一個線程執行。但是這種政策隻在關鍵區相對來說較短時有效,否則會很低效,并且在極端情況時可能會将一個多線程程式的效果變為一個串行程式。上面展示的這個銀行賬戶問題執行個體遵循這一設計方式,由于<code>deposit</code>和<code>withdraw</code>方法的執行時間是可以忽略的,是以這是可行的。
另一個可以應用這一設計方式的場景是使用<code>monitor</code>進行控制台或者檔案輸出的順序化。
這個例子說明了這種方法的緻命缺點:如果輸出到不同檔案時又該如何處理?如果使用以下代碼是否能正常工作?
答案依賴于<code>writeout</code>函數的調用頻率以及使用的輸出流的數目。盡管在原理上這種方法是次優的。一種更好的解決方案是為每一個輸出流配置設定一個不同的<code>monitor</code>對象或者按照下一節介紹的方式設計一個<code>monitor</code>。
在這種方法中,<code>monitor</code>作為一個互斥量使用,亦即包含一對方法,一個用來允許進入關鍵區(獲得允許),另一個用來通知離開關鍵區(釋放允許)。盡管這看上去像是一種模仿互斥量的加鎖解鎖序列的過于複雜方案,但實際上<code>monitor</code>可以支援較之互斥量關鍵區入口更細粒度的控制。
例如,考慮“吸煙者問題”:它包含三個吸煙者和一個代理者線程。每個吸煙者連續制作香煙并吸完。為了制作香煙,吸煙者需要三種原材料:煙草、紙和火柴。這三個吸煙者中的每一個都隻有一種原材料并且數量是無限的,亦即一個隻擁有紙,一個隻擁有煙草,而最後一個隻擁有火柴。代理者線程擁有這三種原材料且數量是無限的,并且隻随機選擇兩種類型的原材料放到桌子上。随後通知這兩種資源的可用性并阻塞。缺少這兩種原材料的吸煙者可以從桌子上擷取并制作香煙,随後需要一個時間随機的視窗用于制作香煙。一旦完成香煙制作,吸煙者就通知代理者循環這一過程。
需要強調的是,這個問題定義較之典型的問題定義更為複雜。在典型的定義中,代理者隻通知需要的吸煙者開始制作香煙。而在這裡需要吸煙者自己判斷是否能繼續執行。
代碼清單3-19展示了這一問題的解決方案。
這一解決方案包含三個類:一個<code>somker</code>類,用其所需要的原材料類型來進行辨別(第3~5行枚舉了原材料);一個agent類,包含一個預定義的疊代次數(在構造函數中定義);一個<code>monitor</code>類,用于允許其他兩個類進行通信。這一解決方案的關鍵之處如下。
<code>monitor</code>提供了以下兩組公用方法。
吸煙者線程中用到的<code>cansomke和finishedsomking</code>對,對應<code>get permit和helease permit</code>類型的函數。
<code>agent</code>線程中用到<code>newingredient和finishsim</code>方法。
<code>agent</code>調用monitor提供的newingredient方法,傳遞可供使用的原材料類型。<code>monitor</code>喚醒所有的等待線程(第31行)然後強制<code>agent</code>線程阻塞,直到某個吸煙者完成執行(第32行)。
agent線程負責程式終止。在疊代次數指定之後,調用<code>monitor的finishsim</code>方法。這将翻轉<code>monitor</code>内部的一個布爾類型的标志(<code>exitf</code>,第52行),并喚醒所有在條件變量w上等待的線程。
當<code>monitor</code>的<code>cansomke</code>方法傳回值非零時,每個<code>smoker</code>線程執行一個負責終止的循環。後一種方法傳回<code>exitf</code>标志的值。
這個程式的一個運作示例如下: