iOS多線程編程指南(四)線程同步(2)
涉及到線程安全時,一個好的設計是最好的保護。避免共享資源,並盡量減少線程間的相互作用,這樣可以讓它們減少互相的干擾。但是一個完全無干擾的設計是不可能的。在線程必須交互的情況下,你需要使用同步工具,來確保當它們交互的時候是安全的。
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 (0×80 >> (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 (0×80 >> (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
- 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頭文件。
6.使用鎖
鎖是線程編程同步工具的基礎。鎖可以讓你很容易保護代碼中一大塊區域以便你可以確保代碼的正確性。Mac OS X和iOS都位所有類型的應用程序提供了互斥鎖,而Foundation框架定義一些特殊情況下互斥鎖的額外變種。以下個部分顯式了如何使用這些鎖的類型。
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
- pthread_mutex_t mutex;
- void MyInitFunction()
- {
- pthread_mutex_init(&mutex, NULL);
- }
- void MyLockingFunction()
- {
- pthread_mutex_lock(&mutex);
- // Do work.
- pthread_mutex_unlock(&mutex);
- }
注意:上面的代碼只是簡單的顯式了使用一個POSIX線程互斥鎖的步驟。你自己的代碼應該檢查這些函數返回的錯誤碼,並適當的處理它們。
6.2 使用NSLock類
在Cocoa程序中NSLock中實現了一個簡單的互斥鎖。所有鎖(包括NSLock)的接口實際上都是通過NSLocking協議定義的,它定義了lock和unlock方法。你使用這些方法來獲取和釋放該鎖。
除了標准的鎖行為,NSLock類還增加了tryLock和lockBeforeDate:方法。方法tryLock試圖獲取一個鎖,但是如果鎖不可用的時候,它不會阻塞線程。相反,它只是返回NO。而lockBeforeDate:方法試圖獲取一個鎖,但是如果鎖沒有在規定的時間內被獲得,它會讓線程從阻塞狀態變為非阻塞狀態(或者返回NO)。
下面的例子顯式了你可以是NSLock對象來協助更新一個可視化顯式,它的數據結構被多個線程計算。如果線程沒有立即獲的鎖,它只是簡單的繼續計算直到它可以獲得鎖再更新顯式。
6.3 使用@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指令,你必須在你的代碼中啟用異常處理。了如果你不想讓隱式的異常處理例程帶來額外的開銷,你應該考慮使用鎖的類。
關於更多@synchronized指令的信息,參閱The Objective-C Programming Language。
6.4 使用其他Cocoa鎖
以下個部分描述了使用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的鎖住和解鎖方法可以任意組合使用。比如,你可以使用unlockWithCondition:和lock消息,或使用lockWhenCondition:和unlock消息。當然,后面的組合可以解鎖一個鎖但是可能沒有釋放任何等待某特定條件值的線程。
下面的例子顯示了生產者-消費者問題如何使用條件鎖來處理。想象一個應用程序包含一個數據的隊列。一個生產者線程把數據添加到隊列,而消費者線程從隊列中取出數據。生產者不需要等待特定的條件,但是它必須等待鎖可用以便它可以安全的把數據添加到隊列。
- id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
- while(true)
- {
- [condLock lock];
- /* Add data to the queue. */
- [condLock unlockWithCondition:HAS_DATA];
- }
因為初始化條件鎖的值為NO_DATA,生產者線程在初始化的時候可以毫無問題的獲取該鎖。它會添加隊列數據,並把條件設置為HAS_DATA。在隨后的迭代中,生產者線程可以把到達的數據添加到隊列,無論隊列是否為空或依然有數據。唯一讓它進入阻塞的情況是當一個消費者線程充隊列取出數據的時候。
因為消費者線程必須要有數據來處理,它會使用一個特定的條件來等待隊列。當生產者把數據放入隊列時,消費者線程被喚醒並獲取它的鎖。它可以從隊列中取出數據,並更新隊列的狀態。下列代碼顯示了消費者線程處理循環的基本結構。
- 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方法來釋放它。

