第四章 線程同步
應用程序裏面多個線程的存在引起了多個執行線程安全訪問資源的潛在問題。兩個線程同時修改同一資源有可能以意想不到的方式互相干擾。好比,一個線程可能覆蓋其餘線程改動的地方,或讓應用程序進入一個未知的潛在無效狀態。若是你幸運的話,受損的資源可能會致使明顯的性能問題或崩潰,這樣比較容易跟蹤並修復它。然而若是你不走運,資源受損可能致使微妙的錯誤,這些錯誤不會當即顯現出來,而是好久以後纔出現,或者致使其餘可能須要一個底層的編碼來顯著修復的錯誤。css
但涉及到線程安全時,一個好的設計是最好的保護。避免共享資源,並儘可能減小線程間的相互做用,這樣可讓它們減小互相的干擾。可是一個徹底無干擾的設計是不可能的。在線程必須交互的狀況下,你須要使用同步工具,來確保當它們交互的時候是安全的。html
Mac OS X和iOS提供了你可使用的多個同步工具,從提供互斥訪問你程序的有序的事件的工具等。如下個部分介紹了這些工具和如何在代碼中使用他們來影響安全的訪問程序的資源。前端
1.1 同步工具
爲了防止不一樣線程意外修改數據,你能夠設計你的程序沒有同步問題,或你也可使用同步工具。儘管徹底避免出現同步問題相對更好一點,可是幾乎老是沒法實現。如下個部分介紹了你可使用的同步工具的基本類別。linux
1.1.1 原子操做
原子操做是同步的一個簡單的形式,它處理簡單的數據類型。原子操做的優點是它們不妨礙競爭的線程。對於簡單的操做,好比遞增一個計數器,原子操做比使用鎖具備更高的性能優點。ios
Mac OS X和iOS包含了許多在32位和64位執行基本的數學和邏輯運算的操做。這些操做都使用了原子版原本操做比較和交換,測試和設置,測試和清理等。查看支持原子操做的列表,參閱/user/include/libkern/OSAtomic.h頭文件和參見atomic主頁。web
1.1.2 內存屏障和 Volatile 變量
爲了達到最佳性能,編譯器一般會對彙編基本的指令進行從新排序來儘量保持處理器的指令流水線。做爲優化的一部分,編譯器有可能對訪問主內存的指令,若是它認爲這有可能產生不正確的數據時,將會對指令進行從新排序。不幸的是,靠編譯器檢測到全部可能內存依賴的操做幾乎老是不太可能的。若是看似獨立的變量其實是相互影響,那麼編譯器優化有可能把這些變量更新位錯誤的順序,致使潛在不不正確結果。objective-c
內存屏障(memory barrier)是一個使用來確保內存操做按照正確的順序工做的非阻塞的同步工具。內存屏障的做用就像一個柵欄,迫使處理器來完成位於障礙前面的任何加載和存儲操做,才容許它執行位於屏障以後的加載和存儲操做。內存屏障一樣使用來確保一個線程(但對另一個線程可見)的內存操做老是按照預約的順序完成。若是在這些地方缺乏內存屏障有可能讓其餘線程看到看似不可能的結果(好比,內存屏障的維基百科條目)。爲了使用一個內存屏障,你只要在你代碼裏面須要的地方簡單的調用OSMemoryBarrier函數。算法
Volatile 變量適用於獨立變量的另外一個內存限制類型。編譯器優化代碼經過加載這些變量的值進入寄存器。對於本地變量,這一般不會有什麼問題。可是若是一個變量對另一個線程可見,那麼這種優化可能會阻止其餘線程發現變量的任何變化。在變量以前加上關鍵字volatile能夠強制編譯器每次使用變量的時候都從內存裏面加載。若是一個變量的值隨時可能給編譯器沒法檢測的外部源更改,那麼你能夠把該變量聲明爲volatile變量。編程
由於內存屏障和volatile變量下降了編譯器可執行的優化,所以你應該謹慎使用它們,只在有須要的地方時候,以確保正確性。關於更多使用內存屏障的信息,參閱OSMemoryBarrier主頁。
1.1.3 鎖
鎖是最經常使用的同步工具。你能夠是使用鎖來保護臨界區(critical section),這些代碼段在同一個時間只能容許被一個線程訪問。好比,一個臨界區可能會操做一個特定的數據結構,或使用了每次只能一個客戶端訪問的資源。
表4-1列出了程序最常使用的鎖。Mac OS X和iOS提供了這些鎖裏面大部分類型的實現,可是並非所有實現。對於不支持的鎖類型,說明列解析了爲何這些鎖不能直接在平臺上面實現的緣由。
Table 4-1 Lock types
Lock |
Description |
Mutex [互斥鎖] |
A mutually exclusive (or mutex) lock acts as a protective barrier around a resource. A mutex is a type of semaphore that grants access to only one thread at a time. If a mutex is in use and another thread tries to acquire it, that thread blocks until the mutex is released by its original holder. If multiple threads compete for the same mutex, only one at a time is allowed access to it. |
Recursive lock [遞歸鎖] |
A recursive lock is a variant on the mutex lock. A recursive lock allows a single thread to acquire the lock multiple times before releasing it. Other threads remain blocked until the owner of the lock releases the lock the same number of times it acquired it. Recursive locks are used during recursive iterations primarily but may also be used in cases where multiple methods each need to acquire the lock separately. |
Read-write lock |
A read-write lock is also referred to as a shared-exclusive lock. This type of lock is typically used in larger-scale operations and can significantly improve performance if the protected data structure is read frequently and modified only occasionally. During normal operation, multiple readers can access the data structure simultaneously. When a thread wants to write to the structure, though, it blocks until all readers release the lock, at which point it acquires the lock and can update the structure. While a writing thread is waiting for the lock, new reader threads block until the writing thread is finished. The system supports read-write locks using POSIX threads only. For more information on how to use these locks, see the pthread man page. |
Distributed lock |
A distributed lock provides mutually exclusive access at the process level. Unlike a true mutex, a distributed lock does not block a process or prevent it from running. It simply reports when the lock is busy and lets the process decide how to proceed. |
Spin lock |
A spin lock polls its lock condition repeatedly until that condition becomes true. Spin locks are most often used on multiprocessor systems where the expected wait time for a lock is small. In these situations, it is often more efficient to poll than to block the thread, which involves a context switch and the updating of thread data structures. The system does not provide any implementations of spin locks because of their polling nature, but you can easily implement them in specific situations. For information on implementing spin locks in the kernel, see Kernel Programming Guide. |
Double-checked lock |
A double-checked lock is an attempt to reduce the overhead of taking a lock by testing the locking criteria prior to taking the lock. Because double-checked locks are potentially unsafe, the system does not provide explicit support for them and their use is discouraged.[注意系統不顯式支持該鎖類型] |
注意:大部分鎖類型都合併了內存屏障來確保在進入臨界區以前它前面的加載和存儲指令都已經完成。
關於如何使用鎖的信息,參閱」使用鎖」部分。
1.1.4 條件
條件是信號量的另一個形式,它容許在條件爲真的時候線程間互相發送信號。條件一般被使用來講明資源可用性,或用來確保任務以特定的順序執行。當一個線程測試一個條件時,它會被阻塞直到條件爲真。它會一直阻塞直到其餘線程顯式的修改信號量的狀態。條件和互斥鎖(mutex lock)的區別在於多個線程被容許同時訪問一個條件。條件更可能是容許不一樣線程根據一些指定的標準經過的守門人。
一個方式是你使用條件來管理掛起事件的池。事件隊列可能使用條件變量來給等待線程發送信號,此時它們在事件隊列中的時候。若是一個事件到達時,隊列將給條件發送合適信號。若是一個線程已經處於等待,它會被喚醒,屆時它將會取出事件並處理它。若是兩個事件到達隊列的時間大體相同,隊列將會發送兩次信號喚醒兩個線程。
系統經過幾個不一樣的技術來支持條件。然而正確實現條件須要仔細編寫代碼,所以你應該在你本身代碼中使用條件以前查看」使用條件」部分的例子。
1.1.5 執行Selector例程
Cocoa程序包含了一個在一個線程以同步的方式傳遞消息的方便方法。NSObject類聲明方法來在應用的一個活動線程上面執行selector的方法。這些方法容許你的線程以異步的方式來傳遞消息,以確保它們在同一個線程上面執行是同步的。好比,你能夠經過執行selector消息來把一個從你分佈計算的結果傳遞給你的應用的主線程或其餘目標線程。每一個執行selector的請求都會被放入一個目標線程的run loop的隊列裏面,而後請求會按照它們到達的順序被目標線程有序的處理。
關於執行selector例程的總結和更多關於如何使用它們的信息,參閱Cocoa執行Selector源。
1.2 同步的成本和性能
同步幫助確保你代碼的正確性,但同時將會犧牲部分性能。甚至在無爭議的狀況下,同步工具的使用將在後面介紹。鎖和原子操做一般包含了內存屏障和內核級別同步的使用來確保代碼正確被保護。若是,發生鎖的爭奪,你的線程有可能進入阻塞,在體驗上會產生更大的遲延。
表4-2列出了在無爭議狀況下使用互斥鎖和原子操做的近似的相關成本。這些測試的平均值是使用了上千的樣本分析出的結果。隨着線程建立時間的推移,互斥採集時間(即便在無爭議狀況下)可能相差也很大,這依賴於進程的加載,計算機的處理速度和系統和程序現有可用的內存。
Table 4-2 Mutex and atomic operation costs
Item |
Approximate cost |
Notes |
Mutex acquisition time |
Approximately 0.2 microseconds |
This is the lock acquisition time in an uncontested case. If the lock is held by another thread, the acquisition time can be much greater. The figures were determined by analyzing the mean and median values generated during mutex acquisition on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running Mac OS X v10.5. |
Atomic compare-and-swap |
Approximately 0.05 microseconds |
This is the compare-and-swap time in an uncontested case. The figures were determined by analyzing the mean and median values for the operation and were generated on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running Mac OS X v10.5. |
當設計你的併發任務時,正確性是最重要的因素,可是也要考慮性能因素。代碼在多個線程下面正確執行,但比相同代碼在當線程執行慢,這是難以改善的。若是你是改造已有的單線程應用,你應該始終給關鍵任務的性能設置測量基線。當增長額外線程後,對相同的任務你應該採起新的測量方法並比較多線程和單線程狀況下的性能情況。在改變代碼以後,線程並無提升性能,你應該須要從新考慮具體的實現或同時使用線程。
關於性能的信息和收集指標的工具,參閱Performance Overview。關於鎖原子成本的特定信息,參閱」線程成本」部分。
1.3 線程安全和信號量
當涉及到多線程應用程序時,沒有什麼比處理信號量更使人恐懼和困惑的了。信號量是底層BSD機制,它能夠用來傳遞信息給進程或以某種方式操縱它。一些應用程序使用信號量來檢測特定事件,好比子進程的消亡。系統使用信號量來終止失控進程,和做爲其餘類型的通訊消息。
使用信號量的問題並非你要作什麼,而是當你程序是多線程的時候它們的行爲。在當線程應用程序裏面,全部的信號量處理都在主線程進行。在多線程應用程序裏面,信號量被傳遞到剛好運行的線程,而不依賴於特定的硬件錯誤(好比非法指令)。若是多個線程同時運行,信號量被傳遞到任何一個系統挑選的線程。換而言之,信號量能夠傳遞給你應用的任何線程。
在你應用程序裏面實現信號量處理的第一條規則是避免假設任一線程處理信號量。若是一個指定的線程想要處理給定的信號,你須要經過某些方法來通知該線程信號什麼時候到達。你不能只是假設該線程的一個信號處理例程的安裝會致使信號被傳遞到同一線程裏面。
關於更多信號量的信息和信號量處理例程的安裝信息,參見signal和sigaction主頁。
1.4 線程安全設計的技巧
同步工具是讓你代碼安全的有用方法,可是它們並不是靈丹妙藥。使用太多鎖和其餘同步的類型原語和非多線程相比明顯會下降你應用的線程性能。在性能和安全之間尋找平衡是一門須要經驗的藝術。如下各部分提供幫助你爲你應用選擇合適的同步級別的技巧。
1.4.1 徹底避免同步
對於你新的項目,甚至已有項目,設計你的代碼和數據結構來避免使用同步是一個很好的解決辦法。雖然鎖和其餘類型同步工具頗有用,可是它們會影響任何應用的性能。並且若是總體設計致使特定資源的高競爭,你的線程可能須要等待更長時間。
實現併發最好的方法是減小你併發任務之間的交互和相互依賴。若是每一個任務在它本身的數據集上面操做,那它不須要使用鎖來保護這些數據。甚至若是兩個任務共享一個普通數據集,你能夠查看分區方法,它們設置或提供拷貝每一項任務的方法。固然,拷貝數據集自己也須要成本,因此在你作出決定前,你須要權衡這些成本和使用同步工具形成的成本那個更能夠接受。
1.4.2 瞭解同步的限制
同步工具只有當它們被用在應用程序中的全部線程是一致時纔是有效的。若是你建立了互斥鎖來限制特定資源的訪問,你全部線程都必須在試圖操縱資源前得到同一互斥鎖。若是不這樣作致使破壞一個互斥鎖提供的保護,這是編程的錯誤。
1.4.3 注意對代碼正確性的威脅
當你使用鎖和內存屏障時,你應該老是當心的把它們放在你代碼正確的地方。即便有條件的鎖(彷佛很好放置)也可能會讓你產生一個虛假的安全感。如下一系列例子試圖經過指出看似無害的代碼的漏洞來舉例說明該問題。其基本前提是你有一個可變的數組,它包含一組不可變的對象集。假設你想要調用數組中第一個對象的方法。你可能會作相似下面那樣的代碼:
1
2
3
4
5
6
7
8
9
|
NSLock
* arrayLock = GetArrayLock();
NSMutableArray
* myArray = GetSharedArray();
id
anObject;
[arrayLock
lock
];
anObject = [myArray
objectAtIndex
:0
];
[arrayLock
unlock
];
[anObject
doSomething
];
|
由於數組是可變的,全部數組周圍的鎖防止其餘線程修改該數組直到你得到了想要的對象。並且由於對象限制它們自己是不可更改的,因此在調用對象的doSomething方法周圍不須要鎖。
可是上面顯式的例子有一個問題。若是當你釋放該鎖,而在你有機會執行doSomething方法前其餘線程到來並從數組中刪除全部對象,那會發生什麼呢?對於沒有使用垃圾回收的應用程序,你代碼用戶的對象可能已經釋放了,讓anObject對象指向一個非法的內存地址。了修正該問題,你可能決定簡單的從新安排你的代碼,讓它在調用doSomething以後才釋放鎖,以下所示:
1
2
3
4
5
6
7
8
|
NSLock
* arrayLock = GetArrayLock();
NSMutableArray
* myArray = GetSharedArray();
id
anObject;
[arrayLock
lock
];
anObject = [myArray
objectAtIndex
:0
];
[anObject
doSomething
];
[arrayLock
unlock
];
|
經過把doSomething的調用移到鎖的內部,你的代碼能夠保證該方法被調用的時候該對象仍是有效的。不幸的是,若是doSomething方法須要耗費很長的時間,這有可能致使你的代碼保持擁有該鎖很長時間,這會產生一個性能瓶頸。
該代碼的問題不是關鍵區域定義不清,而是實際問題是不可理解的。真正的問題是由其餘線程引起的內存管理的問題。由於它能夠被其餘線程釋放,最好的解決辦法是在釋放鎖以前retain anObject。該解決方案涉及對象被釋放,並無引起一個強制的性能損失。
1
2
3
4
5
6
7
8
9
10
11
|
NSLock
* arrayLock = GetArrayLock();
NSMutableArray
* myArray = GetSharedArray();
id
anObject;
[arrayLock
lock
];
anObject = [myArray
objectAtIndex
:0
];
[anObject
retain
];
[arrayLock
unlock
];
[anObject
doSomething
];
[anObject
release
];
|
儘管前面的例子很是簡單,它們說明了很是重要的一點。當它涉及到正確性時,你須要考慮不只僅是問題的表面。內存管理和其餘影響你設計的因子都有可能由於出現多個線程而受到影響,因此你必須考慮從上到下考慮這些問題。此外,你應該在涉及安全的時候假設編譯器老是出現最壞的狀況。這種意識和警戒性,能夠幫你避免潛在的問題,並確保你的代碼運行正確。
關於更多介紹如何讓你應用程序安全的額外例子,參閱Technical Note TN2059:」Using Collection Classes Safely in Multithreaded Application」。
1.4.4 小心死鎖(Deadlocks)和活鎖(Livelocks)
任什麼時候候線程試圖同時得到多於一個鎖,都有可能引起潛在的死鎖。當兩個不一樣的線程分別保持一個鎖(而該鎖是另一個線程須要的)又試圖得到另外線程保持的鎖時就會發生死鎖。結果是每一個線程都會進入持久性阻塞狀態,由於它永遠不可能得到另外那個鎖。
一個活鎖和死鎖相似,當兩個線程競爭同一個資源的時候就可能發生活鎖。在發生活鎖的狀況裏,一個線程放棄它的第一個鎖並試圖得到第二個鎖。一旦它得到第二個鎖,它返回並試圖再次得到一個鎖。線程就會被鎖起來,由於它花費全部的時間來釋放一個鎖,並試圖獲取其餘鎖,而不作實際的工做。
避免死鎖和活鎖的最好方法是同一個時間只擁有一個鎖。若是你必須在同一時間獲取多於一個鎖,你應該確保其餘線程沒有作相似的事情。
1.4.5 正確使用Volatile變量
若是你已經使用了一個互斥鎖來保護一個代碼段,不要自動假設你須要使用關鍵詞volatile來保護該代碼段的重要的變量。一個互斥鎖包含了內存屏障來確保加載和存儲操做是按照正確順序的。在一個臨界區添加關鍵字volatile到變量上面會強制每次訪問該變量的時候都要從內存裏面從加載。這兩種同步技巧的組合使用在一些特定區域是必須的,可是一樣會致使顯著的性能損失。若是單獨使用互斥鎖已經能夠保護變量,那麼忽略關鍵字volatile。
爲了不使用互斥鎖而不使用volatile變量一樣很重要。一般狀況下,互斥鎖和其餘同步機制是比volatile變量更好的方式來保護數據結構的完整性。關鍵字volatile只是確保從內存加載變量而不是使用寄存器裏面的變量。它不保證你代碼訪問變量是正確的。
1.5 使用原子操做
非阻塞同步的方式是用來執行某些類型的操做而避免擴展使用鎖。儘管鎖是同步兩個線程的很好方式,獲取一個鎖是一個很昂貴的操做,即便在無競爭的狀態下。相比,許多原子操做花費不多的時間來完成操做也能夠達到和鎖同樣的效果。
原子操做可讓你在32位或64位的處理器上面執行簡單的數學和邏輯的運算操做。這些操做依賴於特定的硬件設施(和可選的內存屏障)來保證給定的操做在影響內存再次訪問的時候已經完成。在多線程狀況下,你應該老是使用原子操做,它和內存屏障組合使用來保證多個線程間正確的同步內存。
表4-3列出了可用的原子運算和本地操做和相應的函數名。這些函數聲明在/usr/include/libkern/OSAtomic.h頭文件裏面,在那裏你也能夠找到完整的語法。這些函數的64-位版本只能在64位的進程裏面使用。
Table 4-3 Atomic math and logic operations
Operation |
Function name |
Description |
Add |
OSAtomicAdd32 |
Adds two integer values together and stores the result in one of the specified variables. |
Increment |
OSAtomicIncrement32 |
Increments the specified integer value by 1. |
Decrement |
OSAtomicDecrement32 |
Decrements the specified integer value by 1. |
Logical OR |
Performs a logical OR between the specified 32-bit value and a 32-bit mask. |
|
Logical AND |
Performs a logical AND between the specified 32-bit value and a 32-bit mask. |
|
Logical XOR |
Performs a logical XOR between the specified 32-bit value and a 32-bit mask. |
|
Compare and swap |
OSAtomicCompareAndSwap32 |
Compares a variable against the specified old value. If the two values are equal, this function assigns the specified new value to the variable; otherwise, it does nothing. The comparison and assignment are done as one atomic operation and the function returns a Boolean value indicating whether the swap actually occurred. |
Test and set |
Tests a bit in the specified variable, sets that bit to 1, and returns the value of the old bit as a Boolean value. Bits are tested according to the formula (0x80 >> (n & 7)) of byte((char*)address + (n >> 3)) where n is the bit number and address is a pointer to the variable. This formula effectively breaks up the variable into 8-bit sized chunks and orders the bits in each chunk in reverse. For example, to test the lowest-order bit (bit 0) of a 32-bit integer, you would actually specify 7 for the bit number; similarly, to test the highest order bit (bit 32), you would specify 24 for the bit number. |
|
Test and clear |
Tests a bit in the specified variable, sets that bit to 0, and returns the value of the old bit as a Boolean value. Bits are tested according to the formula (0x80 >> (n & 7)) of byte((char*)address + (n >> 3)) where n is the bit number and address is a pointer to the variable. This formula effectively breaks up the variable into 8-bit sized chunks and orders the bits in each chunk in reverse. For example, to test the lowest-order bit (bit 0) of a 32-bit integer, you would actually specify 7 for the bit number; similarly, to test the highest order bit (bit 32), you would specify 24 for the bit number. |
大部分原子函數的行爲是相對簡單的並應該是你想要的。然而列表4-1顯式了測試-設置和比較-交換操做的原子行爲,它們相對複雜一點。OSAtomicTestAndSet 第一次調用展現瞭如何對一個整形值進行位運算操做,而它的結果和你預期的有差別。最後兩次調用OSAtomicCompareAndSwap32顯式它的行爲。全部狀況下,這些函數都是無競爭的下調用的,此時沒有其餘線程試圖操做這些值。
Listing 4-1 Performing atomic operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int32_t theValue =
0
;
OSAtomicTestAndSet
(0
, &theValue);
// theValue is now 128.
theValue =
0
;
OSAtomicTestAndSet
(7
, &theValue);
// theValue is now 1.
theValue =
0
;
OSAtomicTestAndSet
(15
, &theValue)
// theValue is now 256.
OSAtomicCompareAndSwap32
(256
,
512
, &theValue);
// theValue is now 512.
OSAtomicCompareAndSwap32
(256
,
1024
, &theValue);
// theValue is still 512.
|
關於原子操做的更多信息,參見atomic的主頁和/usr/include/libkern/OSAtomic.h頭文件。
1.6 使用鎖
鎖是線程編程同步工具的基礎。鎖可讓你很容易保護代碼中一大塊區域以便你能夠確保代碼的正確性。Mac OS X和iOS都位全部類型的應用程序提供了互斥鎖,而Foundation框架定義一些特殊狀況下互斥鎖的額外變種。如下個部分顯式瞭如何使用這些鎖的類型。
1.6.1 使用POSIX互斥鎖
POSIX互斥鎖在不少程序裏面很容易使用。爲了新建一個互斥鎖,你聲明並初始化一個pthread_mutex_t的結構。爲了鎖住和解鎖一個互斥鎖,你可使用pthread_mutex_lock和pthread_mutex_unlock函數。列表4-2顯式了要初始化並使用一個POSIX線程的互斥鎖的基礎代碼。當你用完一個鎖以後,只要簡單的調用pthread_mutex_destroy來釋放該鎖的數據結構。
Listing 4-2 Using a mutex lock
1
2
3
4
5
6
7
8
9
10
11
12
|
pthread_mutex_t mutex;
void
MyInitFunction()
{
pthread_mutex_init(&mutex,
NULL
);
}
void
MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}
|
注意:上面的代碼只是簡單的顯式了使用一個POSIX線程互斥鎖的步驟。你本身的代碼應該檢查這些函數返回的錯誤碼,並適當的處理它們。
1.6.2 使用NSLock類
在Cocoa程序中NSLock中實現了一個簡單的互斥鎖。全部鎖(包括NSLock)的接口實際上都是經過NSLocking協議定義的,它定義了lock和unlock方法。你使用這些方法來獲取和釋放該鎖。
除了標準的鎖行爲,NSLock類還增長了tryLock和lockBeforeDate:方法。方法tryLock試圖獲取一個鎖,可是若是鎖不可用的時候,它不會阻塞線程。相反,它只是返回NO。而lockBeforeDate:方法試圖獲取一個鎖,可是若是鎖沒有在規定的時間內被得到,它會讓線程從阻塞狀態變爲非阻塞狀態(或者返回NO)。
下面的例子顯式了你能夠是NSLock對象來協助更新一個可視化顯式,它的數據結構被多個線程計算。若是線程沒有當即獲的鎖,它只是簡單的繼續計算直到它能夠得到鎖再更新顯式。
1
2
3
4
5
6
7
8
9
10
11
|
BOOL
moreToDo =
YES
;
NSLock
*theLock = [[
NSLock
alloc
]
init
];
...
while
(moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if
([theLock
tryLock
]) {
/* Update display used by all threads. */
[theLock
unlock
];
}
}
|
1.6.3 使用@synchronized指令
@synchronized指令是在Objective-C代碼中建立一個互斥鎖很是方便的方法。@synchronized指令作和其餘互斥鎖同樣的工做(它防止不一樣的線程在同一時間獲取同一個鎖)。然而在這種狀況下,你不須要直接建立一個互斥鎖或鎖對象。相反,你只須要簡單的使用Objective-C對象做爲鎖的令牌,以下面例子所示:
1
2
3
4
5
6
7
|
- (
void
)myMethod:(
id
)anObj
{
@synchronized
(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
|
建立給@synchronized指令的對象是一個用來區別保護塊的惟一標示符。若是你在兩個不一樣的線程裏面執行上述方法,每次在一個線程傳遞了一個不一樣的對象給anObj參數,那麼每次都將會擁有它的鎖,並持續處理,中間不被其餘線程阻塞。然而,若是你傳遞的是同一個對象,那麼多個線程中的一個線程會首先得到該鎖,而其餘線程將會被阻塞直到第一個線程完成它的臨界區。
做爲一種預防措施,@synchronized塊隱式的添加一個異常處理例程來保護代碼。該處理例程會在異常拋出的時候自動的釋放互斥鎖。這意味着爲了使用@synchronized指令,你必須在你的代碼中啓用異常處理。了若是你不想讓隱式的異常處理例程帶來額外的開銷,你應該考慮使用鎖的類。
關於更多@synchronized指令的信息,參閱The Objective-C Programming Language。
1.6.4 使用其餘Cocoa鎖
如下個部分描述了使用Cocoa其餘類型的鎖。
使用NSRecursiveLock對象
NSRecursiveLock類定義的鎖能夠在同一線程屢次得到,而不會形成死鎖。一個遞歸鎖會跟蹤它被多少次成功得到了。每次成功的得到該鎖都必須平衡調用鎖住和解鎖的操做。只有全部的鎖住和解鎖操做都平衡的時候,鎖才真正被釋放給其餘線程得到。
正如它名字所言,這種類型的鎖一般被用在一個遞歸函數裏面來防止遞歸形成阻塞線程。你能夠相似的在非遞歸的狀況下使用他來調用函數,這些函數的語義要求它們使用鎖。如下是一個簡單遞歸函數,它在遞歸中獲取鎖。若是你不在該代碼裏使用NSRecursiveLock對象,當函數被再次調用的時候線程將會出現死鎖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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的鎖住和解鎖方法能夠任意組合使用。好比,你可使用unlockWithCondition:和lock消息,或使用lockWhenCondition:和unlock消息。固然,後面的組合能夠解鎖一個鎖可是可能沒有釋聽任何等待某特定條件值的線程。
下面的例子顯示了生產者-消費者問題如何使用條件鎖來處理。想象一個應用程序包含一個數據的隊列。一個生產者線程把數據添加到隊列,而消費者線程從隊列中取出數據。生產者不須要等待特定的條件,可是它必須等待鎖可用以便它能夠安全的把數據添加到隊列。
1
2
3
4
5
6
7
8
|
id
condLock = [[
NSConditionLock
alloc
]
initWithCondition
:NO_DATA];
while
(
true
)
{
[condLock
lock
];
/* Add data to the queue. */
[condLock
unlockWithCondition
:HAS_DATA];
}
|
由於初始化條件鎖的值爲NO_DATA,生產者線程在初始化的時候能夠毫無問題的獲取該鎖。它會添加隊列數據,並把條件設置爲HAS_DATA。在隨後的迭代中,生產者線程能夠把到達的數據添加到隊列,不管隊列是否爲空或依然有數據。惟一讓它進入阻塞的狀況是當一個消費者線程充隊列取出數據的時候。
由於消費者線程必需要有數據來處理,它會使用一個特定的條件來等待隊列。當生產者把數據放入隊列時,消費者線程被喚醒並獲取它的鎖。它能夠從隊列中取出數據,並更新隊列的狀態。下列代碼顯示了消費者線程處理循環的基本結構。
1
2
3
4
5
6
7
8
|
while
(
true
)
{
[condLock
lockWhenCondition
:HAS_DATA];
/* Remove data from the queue. */
[condLock
unlockWithCondition
:(isEmpty ?
NO_DATA
: HAS_DATA)];
// Process the data locally.
}
|
使用NSDistributedLock對象
NSDistributedLock類能夠被多臺主機上的多個應用程序使用來限制對某些共享資源的訪問,好比一個文件。鎖自己是一個高效的互斥鎖,它使用文件系統項目來實現,好比一個文件或目錄。對於一個可用的NSDistributedLock對象,鎖必須由全部使用它的程序寫入。這一般意味着把它放在文件系統,該文件系統能夠被全部運行在計算機上面的應用程序訪問。
不像其餘類型的鎖,NSDistributedLock並無實現NSLocking協議,全部它沒有lock方法。一個lock方法將會阻塞線程的執行,並要求系統以預約的速度輪詢鎖。以其在你的代碼中實現這種約束,NSDistributedLock提供了一個tryLock方法,並讓你決定是否輪詢。
由於它使用文件系統來實現,一個NSDistributedLock對象不會被釋放除非它的擁有者顯式的釋放它。若是你的程序在用戶一個分佈鎖的時候崩潰了,其餘客戶端簡沒法訪問該受保護的資源。在這種狀況下,你可使用breadLock方法來打破現存的鎖以便你能夠獲取它。可是一般應該避免打破鎖,除非你肯定擁有進程已經死亡並不可能再釋放該鎖。
和其餘類型的鎖同樣,當你使用NSDistributedLock對象時,你能夠經過調用unlock方法來釋放它。
1.7 使用條件
條件是一個特殊類型的鎖,你可使用它來同步操做必須處理的順序。它們和互斥鎖有微妙的不一樣。一個線程等待條件會一直處於阻塞狀態直到條件得到其餘線程顯式發出的信號。
因爲微妙之處包含在操做系統實現上,條件鎖被容許返回僞成功,即便實際上它們並無被你的代碼告知。爲了不這些僞信號操做的問題,你應該老是在你的條件鎖裏面使用一個斷言。該斷言是一個更好的方法來肯定是否安全讓你的線程處理。條件簡單的讓你的線程保持休眠直到斷言被髮送信號的線程設置了。
如下部分介紹瞭如何在你的代碼中使用條件。
1.7.1 使用NSCondition類
NSCondition類提供了和POSIX條件相同的語義,可是它把鎖和條件數據結構封裝在一個單一對象裏面。結果是一個你能夠像互斥鎖那樣使用的對象,而後等待特定條件。
列表4-3顯示了一個代碼片斷,它展現了爲等待一個NSCondition對象的事件序列。cocaoCondition變量包含了一個NSCondition對象,而timeToDoWork變量是一個整形,它在其餘線程裏面發送條件信號時當即遞增。
Listing 4-3 Using a Cocoa condition
1
2
3
4
5
6
7
8
9
|
[cocoaCondition
lock
];
while
(timeToDoWork <=
0
)
[cocoaCondition
wait
];
timeToDoWork--;
// Do real work here.
[cocoaCondition
unlock
];
|
列表4-4顯示了用於給Cocoa條件發送信號的代碼,並遞增他斷言變量。你應該在給它發送信號前鎖住條件。
Listing 4-4 Signaling a Cocoa condition
1
2
3
4
|
[cocoaCondition
lock
];
timeToDoWork++;
[cocoaCondition
signal
];
[cocoaCondition
unlock
];
|
1.7.2 使用POSIX條件
POSIX線程條件鎖要求同時使用條件數據結構和一個互斥鎖。經管兩個鎖結構是分開的,互斥鎖在運行的時候和條件結構緊密聯繫在一塊兒。多線程等待某一信號應該老是一塊兒使用相同的互斥鎖和條件結構。修改該成雙結構將會致使錯誤。
列表4-5顯示了基本初始化過程,條件和斷言的使用。在初始化以後,條件和互斥鎖,使用ready_to_go變量做爲斷言等待線程進入一個while循環。僅當斷言被設置而且隨後的條件信號等待線程被喚醒和開始工做。
Listing 4-5 Using a POSIX condition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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()
{
// Lock the mutex.
pthread_mutex_lock(&mutex);
// If the predicate is already set, then the while loop is bypassed;
// otherwise, the thread sleeps until the predicate is set.
while
(ready_to_go ==
false
)
{
pthread_cond_wait(&condition, &mutex);
}
// Do work. (The mutex should stay locked.)
// Reset the predicate and release the mutex.
ready_to_go =
false
;
pthread_mutex_unlock(&mutex);
}
|
信號線程負責設置斷言和發送信號給條件鎖。列表4-6顯示了實現該行爲的代碼。在該例子中,條件被互斥鎖內被髮送信號來防止等待條件的線程間發生競爭條件。
Listing 4-6 Signaling a condition lock
1
2
3
4
5
6
7
8
9
10
11
|
void
SignalThreadUsingCondition()
{
// At this point, there should be work for the other thread to do.
pthread_mutex_lock(&mutex);
ready_to_go =
true
;
// Signal the other thread to begin work.
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
|
注意:上述代碼是顯示使用POSIX線程條件函數的簡單例子。你本身的代碼應該檢測這些函數返回錯誤碼並恰當的處理它們。