天天看點

線程程式設計指南翻譯第四篇(同步)同步

同步

應用程式中存在多個線程會打開有關從多個執行線程安全通路資源的潛在問題。修改相同資源的兩個線程可能會以非預期的方式互相幹擾。例如,一個線程可能會覆寫另一個線程的更改,或者将應用程式置于未知且可能無效的狀态。如果幸運的話,損壞的資源可能會導緻明顯的性能問題或崩潰,相對容易追蹤和修複。但是,如果運氣不好,則損壞可能會導緻細微錯誤,這些錯誤直到很久之後才會顯現,或者錯誤可能需要對基礎編碼進行重大改進。

線上程安全方面,良好的設計是的最佳保護。避免共享資源并最小化線程之間的互動使得這些線程不太可能互相幹擾。但是,并非總能實作完全無幹擾的設計。如果線程必須進行互動,則需要使用同步工具來確定它們在互動時安全地進行互動。

OS X和iOS提供了許多同步工具,從提供互斥通路的工具到在應用程式中正确排序事件的工具。以下部分介紹了這些工具以及如何在代碼中使用它們來影響對程式資源的安全通路。

同步工具

為防止不同的線程意外更改資料,可以将應用程式設計為不存在同步問題,也可以使用同步工具。盡管完全避免同步問題是可取的,但并不總是可行的。以下部分介紹了可供使用的同步工具的基本類别。

原子操作

原子操作是一種簡單的同步形式,适用于簡單的資料類型。原子操作的優點是它們不會阻止執行線程。對于簡單的操作,例如遞增計數器變量,這可以帶來比擷取鎖定更好的性能。

OS X和iOS包含許多操作,可對32位和64位值執行基本的數學和邏輯運算。這些操作包括比較和交換,測試和設定以及測試和清除操作的原子版本。有關受支援的原子操作的清單,請參閱/usr/include/libkern/OSAtomic.h頭檔案或參見atomic手冊頁。

記憶體障礙和volatile變量

為了獲得最佳性能,編譯器通常會重新排序彙編級指令,以使處理器的指令流盡可能完整。作為優化的一部分,編譯器可能會重新排序通路主記憶體的指令,因為它認為這樣做不會生成不正确的資料。遺憾的是,編譯器并不總是能夠檢測所有與記憶體相關的操作。如果看似單獨的變量實際上互相影響,則編譯器優化可能會以錯誤的順序更新這些變量,進而産生可能不正确的結果。

記憶體屏障是一種非阻塞同步工具,用于確定記憶體操作以正确的順序發生。存儲器屏障的作用類似于栅欄,迫使處理器在允許執行位于屏障之後的加載和存儲操作之前完成位于屏障前面的任何加載和存儲操作。記憶體屏障通常用于確定一個線程(但對另一個線程可見)的記憶體操作始終以預期的順序發生。在這種情況下缺乏記憶體屏障可能允許其他線程看到看似不可能的結果。(例如,請參閱Wikipedia條目以了解記憶體障礙。)要使用記憶體屏障,隻需在代碼中的适當位置調用OSMemoryBarrier該函數即可。

volatile變量将另一種類型的記憶體限制應用于各個變量。編譯器通常通過将變量的值加載到寄存器來優化代碼。對于局部變量,這通常不是問題。但是,如果變量在另一個線程中可見,則此類優化可能會阻止其他線程注意到對它的任何更改。将volatile關鍵字應用于變量會強制編譯器在每次使用時從記憶體中加載該變量。可以聲明一個volatile變量,就好像它的值可以随時由編譯器可能無法檢測到的外部源更改。

因為記憶體屏障和volatile變量都會減少編譯器可以執行的優化次數,是以應該謹慎使用它們,并且隻在需要確定正确性的地方使用它們。有關使用記憶體屏障的資訊,請參見 OSMemoryBarrier手冊頁。

鎖是最常用的同步工具之一。可以使用鎖來保護代碼的關鍵部分,這一段代碼,一次隻允許一個線程通路。例如,關鍵部分可能操縱特定資料結構或使用一次最多支援一個用戶端的某些資源。通過在此部分周圍放置一個鎖,可以排除其他線程進行可能影響代碼正确性的更改。

表4-1列出了常用的一些鎖。OS X和iOS提供了大多數這些鎖類型的實作,但不是全部。對于不受支援的鎖類型,說明列說明了直接在平台上未實作這些鎖的原因。

表4-1 鎖定類型

描述
互斥 互斥(或互斥)鎖作為資源周圍的保護屏障。互斥鎖是一種信号量,一次隻允許通路一個線程。如果正在使用互斥鎖并且另一個線程嘗試擷取它,則該線程會阻塞,直到互斥鎖被其原始持有者釋放。如果多個線程競争相同的互斥鎖,則一次隻允許一個通路它。
遞歸鎖定 遞歸鎖是互斥鎖的變體。遞歸鎖允許單個線程在釋放之前多次擷取鎖。其他線程保持阻塞狀态,直到鎖的所有者釋放鎖的次數與擷取鎖的次數相同。在遞歸疊代期間主要使用遞歸鎖,但也可以在多個方法分别需要擷取鎖的情況下使用遞歸鎖。
讀寫鎖 讀寫鎖也稱為共享獨占鎖。這種類型的鎖通常用于大規模操作,并且如果經常讀取受保護的資料結構并且僅偶爾修改,則可以顯着提高性能。在正常操作期間,多個讀取器可以同時通路資料結構。但是,當線程想要寫入結構時,它會阻塞,直到所有讀取器釋放鎖定,此時它擷取鎖定并可以更新結構。當寫入線程正在等待鎖定時,新的讀取器線程會阻塞,直到寫入線程完成。系統僅支援使用POSIX線程的讀寫鎖。有關如何使用這些鎖的更多資訊,請參見pthread手冊頁。
分布式鎖 分布式鎖提供程序級别的互斥通路。與真正的互斥鎖不同,分布式鎖定不會阻止程序或阻止程序運作。它隻是報告鎖何時繁忙,并讓流程決定如何繼續。
自旋鎖 自旋鎖重複輪詢其鎖定條件,直到該條件成立為止。自旋鎖最常用于多處理器系統,其中鎖的預期等待時間很短。在這些情況下,輪詢通常比阻塞線程更有效,這涉及上下文切換和線程資料結構的更新。由于其輪詢性質,系統不提供任何自旋鎖的實作,但可以在特定情況下輕松實作它們。有關在核心中實作自旋鎖的資訊,請參閱“ 核心程式設計指南”。
雙重檢查鎖 雙重檢查鎖是嘗試通過在鎖定之前測試鎖定标準來減少鎖定的開銷。由于雙重檢查鎖可能不安全,是以系統不會為它們提供明确的支援,是以不鼓勵使用它們。

注意: 大多數類型的鎖還包含一個記憶體屏障,以確定在進入臨界區之前完成任何先前的加載和存儲指令。

有關如何使用鎖的資訊,請參閱使用鎖。

條件

條件是另一種類型的信号量,它允許線程在某個條件為真時互相發出信号。條件通常用于訓示資源的可用性或確定以特定順序執行任務。當一個線程測試一個條件時,它會阻塞,除非該條件已經為真。它會一直被阻塞,直到其他一些線程明确改變并發出信号。條件和互斥鎖之間的差別在于可以允許多個線程同時通路該條件。這個條件更像是一個看門人,它根據一些指定的标準讓不同的線程通過門。

可能使用條件的一種方法是管理待處理事件池。當隊列中有事件時,事件隊列将使用條件變量來發出等待線程的信号。如果一個事件到達,隊列将适當地發出信号。如果一個線程已經在等待,它将被喚醒,然後它将從隊列中拉出事件并處理它。如果兩個事件大緻同時進入隊列,則隊列将發出兩次信号以喚醒兩個線程。

該系統為幾種不同技術的條件提供支援。但是,正确實作條件需要仔細編碼,是以在使用條件之前,應該檢視使用條件中的示例。

執行選擇器例程

Cocoa應用程式具有以同步方式将消息傳遞到單個線程的便捷方式。在NSObject類聲明對應用程式的活動線程的一個執行選擇器方法。這些方法允許線程異步傳遞消息,并保證它們将由目标線程同步執行。例如,您可以使用執行選擇器消息将分布式計算的結果傳遞到應用程式的主線程或指定的輔助線程。每個執行選擇器的請求都在目标線程的運作循環中排隊,然後按接收順序依次處理請求。

有關執行選擇器例程的摘要以及有關如何使用它們的更多資訊,請參閱Cocoa執行選擇器源

同步成本和性能

同步有助于確定代碼的正确性,但這樣做會犧牲性能。即使在無争議的情況下,使用同步工具也會引入延遲。鎖和原子操作通常涉及使用記憶體屏障和核心級同步來確定代碼得到适當保護。如果存在鎖定争用,您的線程可能會阻塞并經曆更大的延遲。

表4-2列出了與無争議情況下的互斥和原子操作相關的一些近似成本。這些測量值代表數千個樣本的平均時間。與線程建立時間一樣,互斥鎖擷取時間(即使在無争議的情況下)也會因處理器負載,計算機速度以及可用系統和程式存儲器的數量而有很大差異。

表4-2 互斥和原子操作成本

條目 近似成本 說明
互斥擷取時間 大約0.2微秒 這是無争議情況下的鎖定擷取時間。如果鎖定由另一個線程保持,則擷取時間可以更長。這些資料是通過分析在基于Intel的iMac上使用2 GHz Core Duo處理器和運作OS X v10.5的1 GB RAM的互斥鎖擷取時産生的平均值和中值來确定的。
原子比較和交換 大約0.05微秒 這是無争議情況下的比較和交換時間。這些數字是通過分析操作的平均值和中值來确定的,并且是在基于Intel的iMac上生成的,該iMac具有2 GHz Core Duo處理器和運作OS X v10.5的1 GB RAM。

在設計并發任務時,正确性始終是最重要的因素,但您也應該考慮性能因素。在多個線程下正确執行但比在單個線程上運作的相同代碼慢的代碼幾乎不是改進。

如果要對現有的單線程應用程式進行改造,則應始終對關鍵任務的性能進行一組基線測量。添加其他線程後,您應該對這些相同的任務進行新的測量,并将多線程案例的性能與單線程案例進行比較。如果在調整代碼之後,線程不會提高性能,您可能需要重新考慮特定的實作或完全使用線程。

有關性能和收集名額的工具的資訊,請參閱性能概述。有關鎖和原子操作成本的特定資訊,請參閱線程成本。

線程安全和信号

當涉及到線程應用程式時,沒有什麼比處理信号的問題引起更多的恐懼或困惑。信号是一種低級BSD機制,可用于将資訊傳遞給程序或以某種方式對其進行操作。有些程式使用信号來檢測某些事件,例如子程序的死亡。系統使用信号終止失控過程并傳達其他類型的資訊。

信号問題不是它們的作用,而是當應用程式有多個線程時它們的行為。在單線程應用程式中,所有信号處理程式都在主線程上運作。在多線程應用程式中,與特定硬體錯誤(例如非法指令)無關的信号将被傳送到當時正在運作的任何線程。如果同時運作多個線程,則信号将傳遞給系統正在選擇的任何一個。換句話說,信号可以傳遞到應用程式的任何線程。

在應用程式中實作信号處理程式的第一條規則是避免假設哪個線程正在處理信号。如果特定線程想要處理給定信号,則需要在信号到達時通過某種方式通知該線程。您不能隻假設從該線程安裝信号處理程式将導緻信号傳遞到同一線程。

有關信号和安裝信号處理程式的更多資訊,請參見signal和sigaction手冊頁。

線程安全設計提示

同步工具是使代碼線程安全的有用方法,但它們不是靈丹妙藥。使用過多,與其非線程性能相比,鎖和其他類型的同步實際上可以降低應用程式的線程性能。在安全性和性能之間找到适當的平衡是一種需要經驗的藝術。以下部分提供了一些提示,可幫助您為應用程式選擇适當的同步級别。

完全避免同步

對于您正在處理的任何新項目,甚至對于現有項目,設計代碼和資料結構以避免同步是最佳解決方案。盡管鎖和其他同步工具很有用,但它們确實會影響任何應用程式的性能。如果整體設計導緻特定資源之間存在高争用,那麼您的線程可能會等待更長時間。

實作并發的最佳方法是減少并發任務之間的互動和互相依賴關系。如果每個任務都在自己的私有資料集上運作,則不需要使用鎖來保護該資料。即使在兩個任務共享公共資料集的情況下,您也可以檢視設定分區的方法或為每個任務提供自己的副本。當然,複制資料集也有其成本,是以在做出決定之前,您必須權衡這些成本與同步成本。

了解同步的限制

隻有當應用程式中的所有線程一緻地使用同步工具時,它們才有效。如果建立互斥鎖以限制對特定資源的通路,則在嘗試操作資源之前,所有線程都必須擷取相同的互斥鎖。如果不這樣做會失敗互斥鎖提供的保護,并且是程式員錯誤。

意識到編碼正确性的威脅

使用鎖和記憶體屏障時,應始終仔細考慮它們在代碼中的位置。即使看起來很好的鎖也會讓你陷入虛假的安全感。下面的一系列示例試圖通過指出看似無害的代碼中的缺陷來說明這個問題。基本前提是你有一個包含一組不可變對象的可變數組。假設您要調用數組中第一個對象的方法。您可以使用以下代碼執行此操作:

NSLock * arrayLock = GetArrayLock();
NSMutableArray * myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
 
[anObject doSomething];
           

因為數組是可變的,是以數組周圍的鎖定會阻止其他線程修改數組,直到獲得所需的對象。并且因為您檢索的對象本身是不可變的,是以在調用該doSomething方法時不需要鎖定。

但是,前面的例子存在問題。如果您釋放鎖并且另一個線程進入并在您有機會執行該doSomething方法之前從陣列中删除所有對象,會發生什麼?在沒有垃圾收集的應用程式中,您的代碼所持有的對象可能會被釋放,進而anObject指向無效的記憶體位址。要解決此問題,您可能決定隻是重新安排現有代碼并在調用後釋放鎖定doSomething,如下所示:

NSLock * arrayLock = GetArrayLock();
NSMutableArray * myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];
           

通過doSomething在鎖内部移動調用,您的代碼可確定在調用方法時對象仍然有效。不幸的是,如果該doSomething方法需要很長時間才能執行,這可能會導緻您的代碼長時間保持鎖定,這可能會造成性能瓶頸。

代碼的問題不是關鍵區域定義不明确,而是實際問題沒有被了解。真正的問題是僅由其他線程的存在觸發的記憶體管理問題。因為它可以被另一個線程釋放,是以更好的解決方案是anObject在釋放鎖之前保留。該解決方案解決了被釋放對象的真正問題,并且沒有引入潛在的性能損失。

NSLock * arrayLock = GetArrayLock();
NSMutableArray * myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
 
[anObject doSomething];
[anObject release];
           

雖然前面的例子非常簡單,但它們确實說明了非常重要的一點。說到正确性,你必須超越明顯的問題。記憶體管理和設計的其他方面也可能受到多線程的影響,是以您必須預先考慮這些問題。此外,你應該總是假設編譯器在安全方面會做最糟糕的事情。這種意識和警惕應該可以幫助您避免潛在的問題,并確定您的代碼正常運作。

有關如何使程式具有線程安全性的其他示例,請參閱“線程安全摘要”。

注意死鎖和活鎖

每當線程同時嘗試擷取多個鎖時,就有可能發生死鎖。當兩個不同的線程持有另一個需要的鎖,然後嘗試擷取另一個線程持有的鎖時,就會發生死鎖。結果是每個線程永久阻塞,因為它永遠不會獲得另一個鎖。

活鎖類似于死鎖,當兩個線程競争同一組資源時發生。在活鎖情況下,線程放棄其第一個鎖以嘗試擷取其第二個鎖。一旦獲得第二個鎖,它就會傳回并嘗試再次擷取第一個鎖。它鎖定,因為它花費所有時間釋放一個鎖并試圖獲得另一個鎖,而不是做任何實際的工作。

避免死鎖和活鎖情況的最佳方法是一次隻能鎖定一個鎖。如果您必須一次獲得多個鎖,則應確定其他線程不會嘗試執行類似的操作。

正确使用volatile變量

如果您已經使用互斥鎖來保護代碼段,請不要自動假設您需要使用volatile關鍵字來保護該部分中的重要變量。互斥鎖包括一個記憶體屏障,以確定正确的加載和存儲操作順序。将volatile關鍵字添加到關鍵部分中的變量會強制每次通路時從記憶體加載該值。在特定情況下,兩種同步技術的組合可能是必要的,但也會導緻顯着的性能損失。如果僅使用互斥鎖足以保護變量,則省略volatile關鍵字。

同樣重要的是,不要使用volatile變量來避免使用互斥鎖。通常,互斥體和其他同步機制是比volatile變量更好的保護資料結構完整性的方法。的volatile關鍵字僅確定一個變量被從存儲器加載,而不是存儲在寄存器中。它不能確定代碼正确通路變量

使用原子操作

非阻塞同步是一種執行某些類型操作并避免鎖定費用的方法。盡管鎖是同步兩個線程的有效方法,但擷取鎖是一種相對昂貴的操作,即使在無争議的情況下也是如此。相比之下,許多原子操作隻需要一小部分時間就可以完成,并且可以像鎖一樣有效。

通過原子操作,您可以對32位或64位值執行簡單的數學和邏輯運算。這些操作依賴于特殊的硬體指令(和可選的記憶體屏障),以確定在再次通路受影響的記憶體之前完成給定的操作。在多線程情況下,您應始終使用包含記憶體屏障的原子操作,以確定線上程之間正确同步記憶體。

表4-3列出了可用的原子數學和邏輯運算以及相應的函數名稱。這些函數都在/usr/include/libkern/OSAtomic.h頭檔案中聲明,您也可以在其中找到完整的文法。這些功能的64位版本僅在64位程序中可用。

表4-3 原子數學和邏輯運算

操作 函數名 描述
OSAtomicAdd32,OSAtomicAdd32Barrier,OSAtomicAdd64,OSAtomicAdd64Barrier 将兩個整數值一起添加,并将結果存儲在其中一個指定變量中。
遞增 OSAtomicIncrement32,OSAtomicIncrement32Barrier,OSAtomicIncrement64,OSAtomicIncrement64Barrier 将指定的整數值遞增1。
遞減 OSAtomicDecrement32,OSAtomicDecrement32Barrier,OSAtomicDecrement64,OSAtomicDecrement64Barrier 将指定的整數值減1
邏輯或 OSAtomicOr32,OSAtomicOr32Barrier 在指定的32位值和32位掩碼之間執行邏輯OR。
邏輯和 OSAtomicAnd32,OSAtomicAnd32Barrier 在指定的32位值和32位掩碼之間執行邏輯AND。
邏輯異或 OSAtomicXor32,OSAtomicXor32Barrier 在指定的32位值和32位掩碼之間執行邏輯XOR。
比較和交換 OSAtomicCompareAndSwap32,OSAtomicCompareAndSwap32Barrier,OSAtomicCompareAndSwap64,OSAtomicCompareAndSwap64Barrier,OSAtomicCompareAndSwapPtr,OSAtomicCompareAndSwapPtrBarrier,OSAtomicCompareAndSwapInt,OSAtomicCompareAndSwapIntBarrier,OSAtomicCompareAndSwapLong,OSAtomicCompareAndSwapLongBarrier 将變量與指定的舊值進行比較。如果兩個值相等,則此函數将指定的新值配置設定給變量; 否則,它什麼都不做。比較和指派作為一個原子操作完成,函數傳回一個布爾值,訓示交換是否實際發生。
測試和設定 OSAtomicTestAndSet,OSAtomicTestAndSetBarrier 在指定變量中測試一下,将該位設定為1,并将舊位的值作為布爾值傳回。比特根據(0x80 >> (n & 7)) 位元組的公式進行測試,((char*)address + (n >> 3))其中n是位數,address是指向變量的指針。該公式有效地将變量分解為8位大小的塊,并反向排序每個塊中的位。例如,要測試32位整數的最低位(位0),實際上将為位号指定7; 同樣,要測試最高位(位32),您可以為位号指定24。
測試清除 OSAtomicTestAndClear,OSAtomicTestAndClearBarrier 在指定變量中測試一下,将該位設定為0,并将舊位的值作為布爾值傳回。比特根據(0x80 >> (n & 7)) 位元組的公式進行測試,((char*)address + (n >> 3))其中n是位數,address是指向變量的指針。該公式有效地将變量分解為8位大小的塊,并反向排序每個塊中的位。例如,要測試32位整數的最低位(位0),實際上将為位号指定7; 同樣,要測試最高位(位32),您可以為位号指定24。

大多數原子函數的行為應該是相對簡單的,你會期望的。但是,清單4-1顯示了原子測試和設定以及比較和交換操作的行為,這些操作稍微複雜一些。對該OSAtomicTestAndSet函數的前三次調用示範了如何在整數值上使用位操作公式,其結果可能與您期望的不同。最後兩個調用顯示了該OSAtomicCompareAndSwap32函數的行為。在所有情況下,當沒有其他線程正在操作值時,在無争議的情況下調用這些函數。

清單4-1 執行原子操作

int32_t  theValue = 0;
OSAtomicTestAndSet(0, &theValue);
// theValue現在是128。
 
theValue = 0;
OSAtomicTestAndSet(7, &theValue);
// theValue現在是1。
 
theValue = 0;
OSAtomicTestAndSet(15, &theValue)
// theValue現在是256。
 
OSAtomicCompareAndSwap32(256, 512, &theValue);
// theValue現在是512。
 
OSAtomicCompareAndSwap32(256, 1024, &theValue);
// theValue仍然是512。
           

有關原子操作的資訊,請參見atomic手冊頁和/usr/include/libkern/OSAtomic.h頭檔案。

使用鎖

鎖是線程程式設計的基本同步工具。使用鎖可以輕松保護大部分代碼,進而確定代碼的正确性。OS X和iOS為所有應用程式類型提供基本的互斥鎖,而Foundation架構為特殊情況定義了互斥鎖的一些其他變體。以下部分介紹如何使用其中幾種鎖類型。

使用POSIX Mutex鎖

POSIX互斥鎖從任何應用程式都非常容易使用。要建立互斥鎖,請聲明并初始化pthread_mutex_t結構。要鎖定和解鎖互斥鎖,請使用pthread_mutex_lock 和 pthread_mutex_unlock功能。清單4-2顯示了初始化和使用POSIX線程互斥鎖所需的基本代碼。完成鎖定後,隻需撥打電話即可pthread_mutex_destroy 釋放鎖資料結構。

清單4-2 使用互斥鎖

pthread_mutex_t mutex;
void MyInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
}
 
void MyLockingFunction()
{
    pthread_mutex_lock(&mutex);
    // 做工作
    pthread_mutex_unlock(&mutex);
}
           

注意: 前面的代碼是一個簡化的示例,旨在顯示POSIX線程互斥函數的基本用法。您自己的代碼應該檢查這些函數傳回的錯誤代碼并适當地處理它們。

使用NSLock類

一個 NSLock對象為Cocoa應用程式實作了一個基本的互斥鎖。所有鎖(包括NSLock)的接口實際上由NSLocking協定定義,協定定義lock和unlock方法。您可以像使用任何互斥鎖一樣使用這些方法來擷取和釋放鎖定。

除了标準的鎖定行為,NSLock該類還添加了tryLock 和 lockBeforeDate:方法。該tryLock方法嘗試擷取鎖定但不阻止鎖定是否不可用; 相反,該方法隻是傳回NO。如果在指定的時間限制内未擷取鎖定,則lockBeforeDate:該方法嘗試擷取鎖定但取消阻塞線程(并傳回NO)。

以下示例顯示如何使用NSLock對象協調可視顯示的更新,其資料由多個線程計算。如果線程無法立即擷取鎖定,它隻會繼續計算,直到它可以擷取鎖定并更新顯示。

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
    /* 做另一個計算增量 */
    /* 直到沒有其他事可做。 */
    if ([theLock tryLock]) {
        /* 更新所有線程使用的顯示。*/
        [theLock unlock];
    }
}

           

使用@synchronized指令

該@synchronized指令是在Objective-C代碼中動态建立互斥鎖的便捷方式。該@synchronized指令執行任何其他互斥鎖将執行的操作 - 它可以防止不同的線程同時擷取相同的鎖。但是,在這種情況下,您不必直接建立互斥鎖或鎖定對象。相反,您隻需使用任何Objective-C對象作為鎖定标記,如以下示例所示:

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}
           

傳遞給@synchronized指令的對象是用于區分受保護塊的唯一辨別符。如果在兩個不同的線程中執行上述方法,則anObj在每個線程上為參數傳遞一個不同的對象,每個線程都會鎖定并繼續處理,而不會被另一個阻塞。但是,如果在兩種情況下都傳遞相同的對象,則其中一個線程将首先擷取鎖定,另一個線程将阻塞,直到第一個線程完成關鍵部分。

作為預防措施,該@synchronized塊隐式地向受保護代碼添加異常處理程式。如果抛出異常,此處理程式會自動釋放互斥鎖。這意味着為了使用該@synchronized指令,還必須在代碼中啟用Objective-C異常處理。如果您不希望由隐式異常處理程式引起額外開銷,則應考慮使用鎖類。

有關該@synchronized指令的更多資訊,請參閱Objective-C程式設計語言。

使用其他可可鎖

以下部分描述了使用其他幾種Cocoa鎖的過程。

使用NSRecursiveLock對象

該NSRecursiveLock類定義一個鎖,可以由同一個線程多次擷取,而不會導緻線程死鎖。遞歸鎖定會跟蹤成功擷取的次數。鎖定的每次成功擷取必須通過相應的調用來解鎖鎖定。隻有當所有鎖定和解鎖調用都平衡時,鎖才會實際釋放,以便其他線程可以擷取它。

顧名思義,這種類型的鎖通常在遞歸函數中使用,以防止遞歸阻塞線程。您可以類似地在非遞歸情況下使用它來調用其語義要求它們也接受鎖定的函數。這是一個簡單的遞歸函數的例子,它通過遞歸擷取鎖。如果您沒有NSRecursiveLock為此代碼使用對象,則在再次調用該函數時該線程将會死鎖。

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
 
void MyRecursiveFunction(int value)
{
    [theLock lock];
    if (value != 0)
    {
        --value;
        MyRecursiveFunction(value);
    }
    [theLock unlock];
}
 
MyRecursiveFunction(5);
           

注意: 由于在所有鎖定調用與解鎖調用平衡之前不會釋放遞歸鎖定,是以您應仔細權衡使用性能鎖定對潛在性能影響的決策。長時間保持鎖定會導緻其他線程阻塞,直到遞歸完成。如果您可以重寫代碼以消除遞歸或消除使用遞歸鎖定的需要,則可以獲得更好的性能。

使用NSConditionLock對象

一個NSConditionLock對象定義一個互斥鎖可以鎖定的,并用特定的值解鎖。您不應該将此類型的鎖與條件混淆(請參閱條件)。這種行為有點類似于條件,但實作方式卻截然不同。

通常,NSConditionLock當線程需要以特定順序執行任務時,例如當一個線程生成另一個線程消耗的資料時,您使用對象。當生産者正在執行時,消費者使用特定于您的程式的條件擷取鎖。(條件本身隻是您定義的整數值。)當生成器完成時,它解鎖鎖定并将鎖定條件設定為适當的整數值以喚醒使用者線程,然後繼續處理資料。

NSConditionLock對象響應的鎖定和解鎖方法可以任意組合使用。例如,您可以将lock消息與unlockWithCondition:或者a lockWhenCondition:消息unlock。當然,後一種組合解鎖了鎖,但可能不釋放任何等待特定條件值的線程。

以下示

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
 
while(true)
{
    [condLock lock];
    /* 将資料添加到隊列中。 */
    [condLock unlockWithCondition:HAS_DATA];
}
           

因為鎖的初始條件設定為NO_DATA,生産者線程最初應該沒有問題擷取鎖。它用資料填充隊列并将條件設定為HAS_DATA。在後續疊代期間,生産者線程可以在到達時添加新資料,無論隊列是空還是仍有一些資料。它阻止的唯一時間是消費者線程從隊列中提取資料。

由于使用者線程必須具有要處理的資料,是以它使用特定條件在隊列上等待。當生産者将資料放入隊列時,消費者線程會喚醒并擷取其鎖定。然後,它可以從隊列中提取一些資料并更新隊列狀态。以下示例顯示了使用者線程的處理循環的基本結構。

while (true)
{
    [condLock lockWhenCondition:HAS_DATA];
    /* 從隊列中删除資料。 */
    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
 
    // 在本地處理資料。
}
           

使用NSDistributedLock對象

該NSDistributedLock類 可以由多個主機上的多個應用程式使用,以限制對某些共享資源(如檔案)的通路。鎖本身實際上是一個互斥鎖,它使用檔案系統項(例如檔案或目錄)實作。對于NSDistributedLock可以使用的對象,鎖必須可由所有使用它的應用程式寫入。這通常意味着将其放在一個檔案系統上,該檔案系統可供運作該計算機的所有應用程式通路。

與其他類型的鎖不同,NSDistributedLock不符合NSLocking協定,是以沒有lock方法。lock方法将阻塞線程的執行,并要求系統以預定的速率來輪詢鎖。而不是對你的代碼施加這種懲罰,NSDistributedLock提供了一個tryLock 方法,讓您決定是否輪詢。

因為它是使用檔案系統實作的,是以NSDistributedLock除非所有者明确釋放它,否則不會釋放對象。如果您的應用程式在持有分布式鎖定時崩潰,則其他用戶端将無法通路受保護資源。在這種情況下,你可以使用breakLock打破現有鎖定的方法,以便您可以擷取它。但是,通常應該避免斷開鎖定,除非您确定擁有過程已經死亡并且無法釋放鎖定。

與其他類型的鎖一樣,當您使用NSDistributedLock完對象時,可以通過調用該unlock方法來釋放它。

使用條件

條件是一種特殊類型的鎖,可用于同步操作必須進行的順序。它們以微妙的方式與互斥鎖不同。等待條件的線程将保持阻塞狀态,直到該條件由另一個線程顯式發出信号。

由于實作作業系統所涉及的微妙之處,條件鎖允許以虛假的成功傳回,即使它們實際上沒有通過您的代碼發出信号。為避免這些虛假信号引起的問題,您應始終将謂詞與條件鎖一起使用。謂詞是一種更具體的方法,用于确定線程是否安全。該條件隻是讓您的線程保持睡眠狀态,直到可以通過信令線程設定謂詞。

以下部分介紹如何在代碼中使用條件。

使用NSCondition類

的NSCondition類提供相同的語義POSIX的條件,但在單個對象包裝兩者所需的鎖和條件資料結構。結果是一個對象,您可以像互斥鎖一樣鎖定,然後像條件一樣等待。

清單4-3顯示了一個代碼片段,示範了等待NSCondition對象的事件序列。該cocoaCondition變量包含NSCondition對象和timeToDoWork變量是從另一個線程遞增之前立即信令的條件的整數。

清單4-3 使用Cocoa條件

[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];
 
timeToDoWork--;
 
// 在這裡做真正的工作
 
[cocoaCondition unlock];
           

清單4-4顯示了用于表示Cocoa條件并增加謂詞變量的代碼。在發出信号之前,您應該始終鎖定條件。

清單4-4 發出Cocoa信号的信号

[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
           

使用POSIX條件

POSIX線程條件鎖需要使用條件資料結構和互斥鎖。盡管兩個鎖結構是分開的,但互斥鎖在運作時與條件結構緊密相關。等待信号的線程應始終使用相同的互斥鎖和條件結構。更改配對可能會導緻錯誤。

清單4-5顯示了條件和謂詞的基本初始化和用法。在初始化條件和互斥鎖之後,等待線程使用ready_to_go變量作為其謂詞進入while循環。隻有在設定了謂詞并且随後發出條件信号時,等待線程才會喚醒并開始執行其工作。

清單4-5 使用POSIX條件

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean     ready_to_go = true;
 
void MyCondInitFunction()
{
    pthread_mutex_init(&mutex);
    pthread_cond_init(&condition, NULL);
}
 
void MyWaitOnConditionFunction()
{
    // 鎖定互斥鎖。
    pthread_mutex_lock(&mutex);
 
    // 如果已設定謂詞,則繞過while循環;
    // 否則,線程會一直睡眠,直到設定了謂詞。
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
 
    // 做工作。(互斥鎖應保持鎖定狀态。)
 
    // 重置謂詞并釋放互斥鎖。
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}
           

信号線程負責設定謂詞和将信号發送到條件鎖。 清單4-6顯示了實作此行為的代碼。在此示例中,在互斥鎖内部發出條件信号,以防止在等待條件的線程之間發生競争條件。

清單4-6 發出條件鎖定信号

void SignalThreadUsingCondition()
{
    // 此時,應該有其他線程可以做的工作。
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
 
    // 發信号通知另一個線程開始工作。
    pthread_cond_signal(&condition);
 
    pthread_mutex_unlock(&mutex);
}
           

注意: 前面的代碼是一個簡化的示例,旨在顯示POSIX線程條件函數的基本用法。您自己的代碼應該檢查這些函數傳回的錯誤代碼并适當地處理它們。