由于文章長(zhǎng)度限制,本文作為[譯]線程編程指南(二)后續(xù)部分。
線程安全技巧
同步工具是保證代碼線程安全的有效方式,但它不是萬能藥。使用太多鎖或者其他類型的同步原語實(shí)際上會(huì)導(dǎo)致應(yīng)用多線程的性能反而不如非多線程時(shí)的性能。找到安全與性能之間的平衡點(diǎn)是一門需要經(jīng)驗(yàn)的藝術(shù)。下列章節(jié)將為你的應(yīng)用選擇合適的同步等級(jí)提供幫助建議。
避免同步
對(duì)于你工作的任何新項(xiàng)目,甚至對(duì)現(xiàn)有的項(xiàng)目,設(shè)計(jì)代碼和數(shù)據(jù)結(jié)構(gòu)來避免同步使用可能是最好的解決方案。雖然鎖和其他同步工具都很有用,但它們確實(shí)會(huì)影響任何應(yīng)用程序的性能。如果總體設(shè)計(jì)會(huì)導(dǎo)致特定資源之間的高度競(jìng)爭(zhēng),你的線程甚至?xí)却L(zhǎng)的時(shí)間。
實(shí)現(xiàn)并發(fā)的最佳方法是減少并發(fā)任務(wù)之間的交互和相互依賴關(guān)系。如果每個(gè)任務(wù)都在它自己的私有數(shù)據(jù)集上運(yùn)行,則不需要使用鎖來保護(hù)數(shù)據(jù)。即使在兩個(gè)任務(wù)共享一個(gè)共同的數(shù)據(jù)集的情況下,你也可以為每個(gè)任務(wù)提供自己的備份。當(dāng)然,復(fù)制數(shù)據(jù)集也有它的成本,所以你在作出決定之前必須權(quán)衡這些成本和同步的成本。
理解同步的局限性
同步工具只有在使用多線程的應(yīng)用中才會(huì)有效。如果你創(chuàng)建了一個(gè)互斥鎖來限制某個(gè)特定資源的訪問,所有的線程必須在嘗試操作該資源前請(qǐng)求這個(gè)鎖。如果不這樣做,提供這樣的互斥會(huì)另人困惑并成為程序猿的錯(cuò)誤。
注意代碼正確性
當(dāng)使用鎖技術(shù)和內(nèi)存屏障技術(shù)時(shí),你總是應(yīng)該更加小心地在代碼中為其提供位置。即使鎖看起來實(shí)際上可以讓你產(chǎn)生一種虛假的安全感。下面的例子將會(huì)說明這個(gè)問題,并指出在看似無害的代碼中的缺陷?;镜那疤崾?,你有一個(gè)可變數(shù)組包含一組不變的對(duì)象。假設(shè)你想調(diào)用數(shù)組中的第一個(gè)對(duì)象的方法。你可以使用下面的代碼:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
[anObject doSomething];
由于數(shù)組是可變的,保護(hù)數(shù)組的鎖阻止了其他線程對(duì)于數(shù)組的修改直到你完成了從數(shù)組中獲取到想要的對(duì)象。同時(shí)因?yàn)槟惬@取到的對(duì)象是不可變的,所以鎖就沒有必要對(duì)調(diào)用doSomething方法部分的代碼進(jìn)行保護(hù)。
盡管在前面的例子中存在這樣一個(gè)問題。如果釋放鎖時(shí)另一個(gè)線程來移除數(shù)組中的所有對(duì)象,你有機(jī)會(huì)在這之前執(zhí)行doSomething方法?在一個(gè)沒有垃圾收集機(jī)制的應(yīng)用中,代碼中持有的對(duì)象可能被釋放,留下一個(gè)指向無效內(nèi)存地址的指針。要解決這個(gè)問題,你可以簡(jiǎn)單地重新安排你的現(xiàn)有代碼并在調(diào)用doSomething后釋放鎖,如下所示:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];
通過在鎖內(nèi)部調(diào)用doSomething方法,代碼可以保證方法調(diào)用時(shí)對(duì)象仍然有效。不幸的是,如果doSomething方法需要花費(fèi)很長(zhǎng)時(shí)間來執(zhí)行,這將導(dǎo)致代碼長(zhǎng)時(shí)間的持有鎖,并造成性能上的瓶頸。
這段代碼的問題不是臨界區(qū)定義得不好,而真正的問題并沒有理解。真正的問題是由其他線程的存在而觸發(fā)的內(nèi)存管理問題。由于對(duì)象能夠被其他線程釋放,所以更好的解決辦法是在鎖釋放之前持有anObject。該解決方案解決了對(duì)象被釋放的實(shí)際問題,并沒有引入一個(gè)潛在的性能隱患。
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
[anObject doSomething];
[anObject release];
盡管先前的示例事實(shí)上非常簡(jiǎn)單,它們確實(shí)說明了非常重要的一點(diǎn)。說到正確性,你必須考慮到這個(gè)明顯的問題。內(nèi)存管理和其他方面的設(shè)計(jì)也可能會(huì)受到多線程存在的影響,所以你要提前考慮這些問題。此外,當(dāng)涉及到安全問題時(shí),你應(yīng)該經(jīng)常假設(shè)編譯器可能會(huì)做最壞的事情。這種意識(shí)和警惕性應(yīng)該幫助你避免潛在的問題,確保代碼的行為正確。
獲取更多線程安全的示例,請(qǐng)看之后的線程安全總結(jié)。
當(dāng)心死鎖與活鎖
任何時(shí)候一個(gè)線程試圖同時(shí)持有一個(gè)以上的鎖時(shí),有可能發(fā)生死鎖。死鎖發(fā)生在兩個(gè)不同的線程各自持有一個(gè)鎖,而線程同時(shí)需要持有對(duì)方所持有的鎖時(shí)。其結(jié)果是,每個(gè)線程都會(huì)永久地被阻塞,因?yàn)樗肋h(yuǎn)無法獲得另一個(gè)鎖。
活鎖和死鎖類似,同樣發(fā)生在兩個(gè)線程競(jìng)爭(zhēng)同一資源時(shí)。在活鎖的情況中,一個(gè)線程放棄其鎖并試圖獲取另一個(gè)鎖。一旦它獲得了另一個(gè)鎖,它又返回并試圖獲得第一個(gè)鎖。這樣一來它會(huì)被鎖住,因?yàn)樗ㄙM(fèi)了所有時(shí)間來釋放一個(gè)鎖并試圖獲得另一個(gè)鎖,而不是做任何實(shí)際工作。
為了同時(shí)避免死鎖和活鎖的情況,最好的辦法是一次只拿一個(gè)鎖。如果你必須同時(shí)獲得一個(gè)以上的鎖,你應(yīng)該確保其他線程不嘗試做相同的事情。
正確使用volatile變量
如果你已經(jīng)使用互斥鎖來保護(hù)一段代碼,不要想當(dāng)然地認(rèn)為你需要使用volatile關(guān)鍵字來保護(hù)該區(qū)域的重要變量。互斥包括了一個(gè)確保已載入和已存儲(chǔ)操作正確順序的內(nèi)存屏障。在臨界區(qū)內(nèi)將變量強(qiáng)制設(shè)置為volatile可以保證每次獲取的值都是來自于內(nèi)存當(dāng)中。在特定情況下這兩種技術(shù)結(jié)合使用也許是必要的,但也導(dǎo)致顯著的性能損失。如果單獨(dú)使用互斥足以保護(hù)變量,請(qǐng)省略使用關(guān)鍵字volatile。
同樣重要的是,在不使用互斥的時(shí)候也不必使用volatile變量。總的來說,互斥和其他同步機(jī)制是比volatile更好的保護(hù)數(shù)據(jù)結(jié)構(gòu)完整性的方式。Volatile關(guān)鍵字只確保一個(gè)變量是從內(nèi)存中而不是寄存器中加載,并不能確保你的代碼可以正確地訪問該變量。
使用原子操作
非阻塞式的同步是一種可以執(zhí)行某些操作并避免鎖消耗的方式。雖然鎖是兩個(gè)線程間一種有效的同步方式,但請(qǐng)求鎖是資源消耗相對(duì)昂貴的操作,即便是在非沖突情況下。相反的,許多原子性操作只占用小部分時(shí)間來完成和鎖同樣有效的操作。
原子操作讓你在32位或64位值上執(zhí)行簡(jiǎn)單的數(shù)學(xué)和邏輯運(yùn)算。這些操作依賴于特殊的硬件指令(和可選的內(nèi)存屏障),以確保在相關(guān)的內(nèi)存再次訪問之前完成既定操作。在多線程的情況下,你應(yīng)該經(jīng)常使用包含內(nèi)存障礙的原子操作以確保這部分存儲(chǔ)在線程之間是正確同步的。
表4-3列舉了可用的原子性數(shù)學(xué)和邏輯操作以及相應(yīng)的函數(shù)名稱。這些函數(shù)全部聲明在/usr/include/libkern/OSAtomic.h頭文件中,你可以在里面找到完整的語法。這些函數(shù)的64位版本只存在與64位的進(jìn)程中。
表4-3 原子性的數(shù)學(xué)和邏輯操作
| 操作 | 函數(shù)名稱 | 描述 |
|---|---|---|
| 加(Add) | OSAtomicAdd32 OSAtomicAdd32Barrier OSAtomicAdd64 OSAtomicAdd64Barrier |
兩個(gè)整型值相加并將結(jié)果賦值給指定變量。 |
| 遞增(Increment) | OSAtomicIncrement32 OSAtomicIncrement32Barrier OSAtomicIncrement64 OSAtomicIncrement64Barrier |
指定整型值加1。 |
| 遞減(Decrement) | OSAtomicDecrement32 OSAtomicDecrement32Barrier OSAtomicDecrement64 OSAtomicDecrement64Barrier |
指定整型值減1。 |
| 邏輯或(Logical OR) | OSAtomicOr32 OSAtomicOr32Barrier |
在32位值和32位掩碼間執(zhí)行邏輯或操作。 |
| 邏輯與(Logical AND) | OSAtomicAnd32 OSAtomicAnd32Barrier |
在32位值和32位掩碼間執(zhí)行邏輯與操作。 |
| 邏輯異或(Logical XOR) | OSAtomicXor32 OSAtomicXor32Barrier |
在32位值和32位掩碼間執(zhí)行邏輯異或操作。 |
| 比較和交換(Compare and swap) | OSAtomicCompareAndSwap32 OSAtomicCompareAndSwap32Barrier OSAtomicCompareAndSwap64 OSAtomicCompareAndSwap64Barrier OSAtomicCompareAndSwapPtr OSAtomicCompareAndSwapPtrBarrier OSAtomicCompareAndSwapInt OSAtomicCompareAndSwapIntBarrier OSAtomicCompareAndSwapLong OSAtomicCompareAndSwapLongBarrier |
對(duì)變量的舊值進(jìn)行比較。如果兩個(gè)值是相等的,這個(gè)函數(shù)將指定新值賦給該變量;否則,它什么也不做。比較和賦值作為一個(gè)原子操作,該函數(shù)會(huì)返回一個(gè)布爾值以表示是否發(fā)生交換。 |
| 測(cè)試和設(shè)置(Test and set) | OSAtomicTestAndSet OSAtomicTestAndSetBarrier |
在指定的變量中測(cè)試一個(gè)位,將該位設(shè)置為1,并將老位的值作為布爾值返回。位根據(jù)公式進(jìn)行測(cè)試(0x80 >> (n & 7))字節(jié)((char*)address + (n >> 3)),n是位號(hào)碼和地址是一個(gè)指針變量。這個(gè)公式有效地分解成8位大小的塊,并在每一個(gè)塊中的位順序反轉(zhuǎn)。例如,為了測(cè)試一個(gè)32位整數(shù)的最低序位(位0),你將實(shí)際指定的位號(hào)為7;同樣,要測(cè)試的最高點(diǎn)位(位32),你將指定24位數(shù)字。 |
| 測(cè)試和清理(Test and clear) | OSAtomicTestAndClear OSAtomicTestAndClearBarrier |
在指定的變量中測(cè)試一個(gè)位,將該位設(shè)置為0,并將老位的值返回布爾值。位根據(jù)公式進(jìn)行測(cè)試(0x80 >> (n & 7))字節(jié)((char*)address + (n >> 3)),n是位號(hào)碼和地址是一個(gè)指針變量。這個(gè)公式有效地分解成8位大小的塊,并在每一個(gè)塊中的位順序反轉(zhuǎn)。例如,為了測(cè)試一個(gè)32位整數(shù)的最低序位(位0),你將實(shí)際指定的位號(hào)為7;同樣,要測(cè)試的最高點(diǎn)位(位32),你將指定24位數(shù)字。 |
大多數(shù)原子函數(shù)的行為應(yīng)該是相對(duì)簡(jiǎn)單并如你所期望的。然而代碼4-1,顯示了原子性的test-and-set以及compare-and-swap操作相對(duì)復(fù)雜的行為。前面三個(gè)調(diào)用OSAtomicTestAndSet函數(shù)來展示位操作公式如何被用于整型值,并且其結(jié)果可能與你所期望的不同。后面兩個(gè)調(diào)用展示了OSAtomicCompareAndSwap32函數(shù)的行為。在所有情況下,這些函數(shù)都是在沒有其他線程操作的值的無沖突情況下調(diào)用。
代碼4-1 執(zhí)行原子操作
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.
更多有關(guān)原子性操作的信息,請(qǐng)查看atomic的man幫助頁或者/usr/include/libkern/OSAtomic.h頭文件。
使用鎖
鎖作為線程編程的基本同步工具。鎖使你能夠很容易地保護(hù)大段代碼,這樣你就可以確保代碼的正確性。OS X和iOS為所有應(yīng)用類型提供了基本的互斥鎖,并且Foundation Framework為特殊的情形定義了額外的變量。下面的章節(jié)將向你展示如何使用這些鎖類型。
使用POSIX的Mutex鎖
POSIX的互斥鎖在任何應(yīng)用中都能夠極其簡(jiǎn)單地使用。為創(chuàng)建互斥鎖,你需要聲明并初始化一個(gè)pthread_mutex_t結(jié)構(gòu)體。為完成鎖和解鎖的操作,你需要使用pthread_mutex_lock和pthread_mutex_unlock函數(shù)。代碼4-2展示了使用POSIX線程互斥鎖所需要初始化的基本代碼。當(dāng)你完成了該鎖的操作時(shí),簡(jiǎn)單地調(diào)用pthread_mutex_destroy函數(shù)來釋放鎖。
代碼4-2 使用互斥鎖
pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}
注意:以上代碼只是一個(gè)展示POSIX線程互斥鎖函數(shù)的簡(jiǎn)單示例。你自己的代碼必須檢查這些函數(shù)返回的錯(cuò)誤碼并正確地處理它們。
使用NSLock
NSLock對(duì)象為Cocoa應(yīng)用實(shí)現(xiàn)了基本的互斥功能。所有鎖(包括NSLock)事實(shí)上由NSLocking協(xié)議定義,該協(xié)議同樣定義了lock和unlock方法。你可以在任何需要互斥的地方使用這些方法來請(qǐng)求鎖以及釋放鎖。
除了標(biāo)準(zhǔn)的鎖操作之外,NSLock類還加入了tryLock和lockBeforeDate:方法。tryLock方法試圖獲取鎖但在所不可用時(shí)并不阻塞線程,而是返回NO。lockBeforeDate:方法在指定時(shí)間內(nèi)鎖不能獲取時(shí)試圖獲取鎖但不阻塞線程(并返回NO)。
下面的示例將向你展示如何使用NSLock來調(diào)節(jié)可視化視圖的更新,視圖更新的數(shù)據(jù)來自于其他線程的計(jì)算結(jié)果。如果線程不能立即請(qǐng)求到鎖,它會(huì)繼續(xù)其計(jì)算操作直到所能夠獲取時(shí)更新顯示。
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];
}
}
使用@synchronized
@synchronized語句是Objective-C代碼中創(chuàng)建互斥鎖的便捷方式。@synchronized語句完成其他互斥鎖應(yīng)該做的事情-它防止不同線程在同一時(shí)間請(qǐng)求相同的鎖。在這種情況下,你沒有必要?jiǎng)?chuàng)建互斥鎖或者直接鎖住一個(gè)對(duì)象。相反地,你可以簡(jiǎn)單地使用任何Objective-C對(duì)象作為鎖令牌,正如下面代碼所示:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
傳遞給@synchronized語句的對(duì)象會(huì)成為區(qū)別受保護(hù)代碼塊的唯一標(biāo)識(shí)。如果你在兩個(gè)線程中執(zhí)行先前的這個(gè)方法,并向每個(gè)線程傳遞不同的對(duì)象作為anObj參數(shù),每個(gè)線程會(huì)獲得這個(gè)鎖并不受阻塞的繼續(xù)執(zhí)行。如果你同時(shí)傳遞同一個(gè)對(duì)象,其中一個(gè)線程會(huì)首先獲得鎖并使得另一個(gè)線程阻塞直到第一個(gè)線程退出了臨界區(qū)。
作為一種預(yù)防措施,@synchronized塊會(huì)向受到保護(hù)的代碼隱式地添加異常處理回調(diào)。該回調(diào)在異常拋出時(shí)會(huì)自動(dòng)釋放互斥鎖。這意味著為了使用@synchronized語句,你必須在代碼中開啟Objective-C的異常處理。如果你不希望由隱式異常處理程序引起額外的開銷,你應(yīng)該考慮使用鎖類。
使用其他的Cocoa鎖
下面的章節(jié)將描述Cocoa其他類型鎖的使用。
使用NSRecursiveLock對(duì)象
NSRecursiveLock類定義了一種可以多次被同一線程請(qǐng)求且不導(dǎo)致線程死鎖的鎖類型。遞歸鎖必須記錄有好多次被成功請(qǐng)求。每次鎖的成功請(qǐng)求必須由相應(yīng)的次數(shù)的鎖和解鎖調(diào)用來平衡。只有當(dāng)所有的鎖和解鎖調(diào)用平衡時(shí)鎖才會(huì)被釋放并繼續(xù)有其他線程請(qǐng)求。
正如其名字暗示的一樣,該類型的鎖通常用于遞歸函數(shù)來防止遞歸操作導(dǎo)致線程的阻塞。在非遞歸的情況下,你可以用它來調(diào)用那些語意上仍希望持有鎖的函數(shù)。下面是一個(gè)遞歸函數(shù)中請(qǐng)求該鎖的代碼示例。如果你不像代碼中那樣使用NSRecursiveLock對(duì)象,你的線程在函數(shù)再次調(diào)用時(shí)產(chǎn)生死鎖。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
}
MyRecursiveFunction(5);
注意:由于遞歸鎖直到全部鎖調(diào)用和解鎖調(diào)用平衡時(shí)才釋放,你應(yīng)該仔細(xì)權(quán)衡使用鎖和這樣做造成潛在的性能影響。在一個(gè)較長(zhǎng)的時(shí)間內(nèi)保持任何鎖會(huì)使其它線程阻塞直到遞歸完成。如果可以重寫代碼來消除遞歸或需要使用的遞歸鎖,則可以實(shí)現(xiàn)更好的性能。
使用NSConditionLock對(duì)象
NSConditionLock類定義了可以根據(jù)特殊值來進(jìn)行鎖和解鎖操作的互斥鎖。你不應(yīng)該將該類型的鎖和之前的條件量混為一談。它和條件量某種意義上講行為相似,但實(shí)現(xiàn)方式完全不同。
通常,你將NSConditionLock對(duì)象用于線程需要執(zhí)行特定順序的任務(wù)時(shí),比如一個(gè)線程生產(chǎn)數(shù)據(jù)而另一個(gè)線程消費(fèi)數(shù)據(jù)。當(dāng)生產(chǎn)者執(zhí)行時(shí),消費(fèi)者請(qǐng)求鎖的條件取決于你的程序。(條件本身僅僅是一個(gè)定義的整型值)當(dāng)生產(chǎn)者完成時(shí),它會(huì)解鎖并將鎖條件置為合適的整型值來喚醒消費(fèi)者線程,消費(fèi)者線程然后收到并處理數(shù)據(jù)。
NSConditionLock對(duì)象中的鎖定和解鎖方法可以任意地組合使用。例如,你可以將鎖定信息配對(duì)給unlockWithCondition:,或者解鎖信息配對(duì)給lockWithCondition:。當(dāng)然,這一組合解鎖但不會(huì)釋放任何線程等待特定的條件值。
下面的示例演示了如何使用條件鎖處理“生產(chǎn)者-消費(fèi)者”問題。設(shè)想應(yīng)用程序包含一個(gè)數(shù)據(jù)隊(duì)列。生產(chǎn)者線程將數(shù)據(jù)添加到隊(duì)列,而消費(fèi)者線程從隊(duì)列中提取數(shù)據(jù)。生產(chǎn)者不需要等待一個(gè)特定的條件,但它必須等待鎖以便它可以安全地添加數(shù)據(jù)到隊(duì)列。
id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
while(true)
{
[condLock lock];
/* Add data to the queue. */
[condLock unlockWithCondition:HAS_DATA];
}
因?yàn)殒i的初始條件設(shè)置為NO_DATA,所以生產(chǎn)者線程期初獲取鎖并不受影響。它將隊(duì)列填充好數(shù)據(jù)并將條件設(shè)置為HAS_DATA。在隨后的迭代中,生產(chǎn)者線程可以在到達(dá)時(shí)添加新的數(shù)據(jù)不管隊(duì)列是否是空的還是有一些數(shù)據(jù)。當(dāng)消費(fèi)者線程從隊(duì)列中提取數(shù)據(jù)時(shí),它阻塞的唯一時(shí)間是消費(fèi)者線程從隊(duì)列中提取數(shù)據(jù)時(shí)。
因?yàn)橄M(fèi)者線程必須要有數(shù)據(jù)處理,它根據(jù)特定的條件等待隊(duì)列。當(dāng)生產(chǎn)者將數(shù)據(jù)放在隊(duì)列上時(shí),消費(fèi)者線程喚醒并請(qǐng)求鎖。然后,它可以從隊(duì)列中提取一些數(shù)據(jù)并更新隊(duì)列狀態(tài)。下面的示例顯示了消費(fèi)者線程處理循環(huán)的基本結(jié)構(gòu)。
while (true)
{
[condLock lockWhenCondition:HAS_DATA];
/* Remove data from the queue. */
[condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
// Process the data locally.
}
使用NSDistributedLock對(duì)象
NSDistributedLock類可用于多個(gè)宿主機(jī)上的多個(gè)應(yīng)用之間來限制某些共享資源的訪問,例如文件。該鎖本身是一種由文件系統(tǒng)(如文件或者目錄)實(shí)現(xiàn)的非常高效的互斥鎖。為使NSDistributedLock對(duì)象可用,該鎖必須由所有的應(yīng)用來使用。這通常意味著把它放在所有計(jì)算機(jī)上的應(yīng)用程序都可以訪問的文件系統(tǒng)中。
不像其他類型的鎖,NSDistributedLock并不遵循NSLocking協(xié)議并且沒有lock方法。lock方法會(huì)阻塞線程的執(zhí)行,并要求系統(tǒng)以一個(gè)預(yù)定的速率輪詢鎖。NSDistributedLock提供tryLock方法讓你決定是否輪詢,而不是在你自己的代碼中這樣做。
由于它使用文件系統(tǒng)來實(shí)現(xiàn),NSDistributedLock對(duì)象直到在持有者顯式地釋放它時(shí)釋放。如果你的應(yīng)用在持有分布式鎖是崩潰了,其他的客戶端將不能對(duì)保護(hù)資源進(jìn)行訪問。在這種情況下,你可以使用breakLock方法來打破既存鎖以便你能夠請(qǐng)求到它。破壞鎖通常是需要避免的,除非你確定鎖的持有者死掉了且不能釋放鎖。
同其他類型的鎖一樣,當(dāng)你用完NSDistributedLock對(duì)象后,可以使用unlock方法來釋放它。
使用條件量
條件量是一種用于同步操作順序的特殊類型的鎖。它與互斥鎖之間只有細(xì)微的差別。線程會(huì)保持阻塞直到其他線程顯式地喚醒條件量。
由于細(xì)節(jié)涉及到操作系統(tǒng)實(shí)現(xiàn),條件量允許假定還鎖成功,即使沒有在代碼中喚醒它們。為了避免這些虛假信號(hào)引起的問題,你應(yīng)該經(jīng)常使用一個(gè)謂詞與你的條件量一起使用。謂詞是一個(gè)更具體的方法,它決定是否安全地為你的線程進(jìn)行處理。條件量簡(jiǎn)單地保持你的線程睡眠,直到謂詞可以被喚醒線程設(shè)置。
下面的章節(jié)將告訴你如何在代碼中使用條件量。
使用NSCondition
NSCondition類提供了和POSIX條件量語意相同,但同時(shí)包裝了鎖和條件數(shù)據(jù)到單個(gè)對(duì)象的數(shù)據(jù)結(jié)構(gòu)。這就使得對(duì)象可以像互斥鎖并且像條件量那樣等待條件。
代碼4-3代碼段展示了為等待NSCondition對(duì)象的事件隊(duì)列。cocoaCondition變量包含一個(gè)NSCondition對(duì)象和timeToDoWork,由其他線程喚醒條件時(shí)自增的整型變量。
代碼4-3 使用Cocoa條件量
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];
代碼4-4展示了喚醒Cocoa條件量并完成謂詞變量自增的代碼。你應(yīng)該總是在喚醒條件量之前鎖住它。
代碼4-4 喚醒Cocoa條件量
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
使用POSIX的條件量
POSIX線程的條件量同時(shí)滿足條件數(shù)據(jù)結(jié)構(gòu)和互斥鎖的功能。盡管兩個(gè)鎖結(jié)構(gòu)各自獨(dú)立,但在運(yùn)行時(shí)互斥鎖緊密地綁定著條件結(jié)構(gòu)。等待一個(gè)信號(hào)的線程應(yīng)該總是一起使用這樣非互斥鎖和條件結(jié)構(gòu)。改變這樣的配對(duì)可能造成錯(cuò)誤。
代碼4-5展示了條件量和謂詞的基本初始化和使用。經(jīng)過初始化的條件量和互斥鎖,等待線程使用ready_to_go變量作為謂詞并進(jìn)入while循環(huán)。只有當(dāng)謂詞被設(shè)置并且緊接著條件量被發(fā)出,等待線程才喚醒并開始做它的工作。
代碼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()
{
// 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);
}
發(fā)信號(hào)線程負(fù)責(zé)設(shè)置謂詞,并將信號(hào)發(fā)送到條件量。代碼4-6顯示了實(shí)現(xiàn)該行為的代碼。在這個(gè)例子中,條件量是在互斥內(nèi)部被喚醒以防止等待條件的線程間的競(jìng)態(tài)條件發(fā)生。
代碼4-6 喚醒條件量
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);
}
注意:以上代碼只是一個(gè)展示POSIX線程條件量函數(shù)的簡(jiǎn)單示例。你自己的代碼必須檢查這些函數(shù)返回的錯(cuò)誤碼并正確地處理它們。
附錄A:線程安全總結(jié)
本附錄描述了OS X和iOS中某些關(guān)鍵框架的高級(jí)別的線程安全。本附錄中的信息是隨時(shí)變更的。
Cocoa
多線程中使用Cocoa的指導(dǎo)如下:
- 不可變(immutable)對(duì)象通常是線程安全的。一旦你創(chuàng)建它們,你可以在線程間安全地傳遞這些對(duì)象。另一方面,可變(mutable)對(duì)象通常不是線程安全的。在多線程應(yīng)用中使用可變對(duì)象,應(yīng)用程序必須正確同步。
- 許多對(duì)象看似“安全”實(shí)則在多線程中使用時(shí)不安全。許多這些對(duì)象可以在任何線程中使用,只要在同一時(shí)間且同一線程。被嚴(yán)格限制在應(yīng)用程序的主線程上的對(duì)象被調(diào)用時(shí)就是如此。
- 應(yīng)用程序的主線程負(fù)責(zé)處理事件。雖然其他線程進(jìn)入事件路徑時(shí)Application Kit將繼續(xù)工作,但它的操作可發(fā)生在事件隊(duì)列之外。
- 如果你想用線程來繪制視圖,使用NSView的
lockFocusIfCanDraw和unlockFocus方法將所有繪制代碼包括進(jìn)來。 - 為了能在Cocoa中使用POSIX線程,你必須首先將應(yīng)用置于多線程模式。
Foundation Framework線程安全
有一種誤解,認(rèn)為Foundation Framework是線程安全的,而Application Kit不是線程安全的。不幸的是,這只是一個(gè)總的概括,有些誤導(dǎo)。每個(gè)框架都有線程安全的區(qū)域和線程不安全的區(qū)域。下面的章節(jié)描述了Foundation Framework的通用的線程安全性。
線程安全的類和函數(shù)
下列的類和函數(shù)通常被認(rèn)為是線程安全的。你可以在多個(gè)線程中使用相同實(shí)例而不需請(qǐng)求鎖。
NSArray
NSAssertionHandler
NSAttributedString
NSCalendarDate
NSCharacterSet
NSConditionLock
NSConnection
NSData
NSDate
NSDecimal 函數(shù)
NSDecimalNumber
NSDecimalNumberHandler
NSDeserializer
NSDictionary
NSDistantObject
NSDistributedLock
NSDistributedNotificationCenter
NSException
NSFileManager (OS X 10.5及后續(xù)版本)
NSHost
NSLock
NSLog/NSLogv
NSMethodSignature
NSNotification
NSNotificationCenter
NSNumber
NSObject
NSPortCoder
NSPortMessage
NSPortNameServer
NSProtocolChecker
NSProxy
NSRecursiveLock
NSSet
NSString
NSThread
NSTimer
NSTimeZone
NSUserDefaults
NSValue
NSXMLParser
對(duì)象的allocation 和 retain count 函數(shù)
Zone 和 memory 函數(shù)
非線程安全的類和函數(shù)
下列的類和函數(shù)通常被認(rèn)為是非線程安全的。大多數(shù)情況下,你可以在多線程環(huán)境使用這些類只要你在同一時(shí)刻同一線程中。
NSArchiver
NSAutoreleasePool
NSBundle
NSCalendar
NSCoder
NSCountedSet
NSDateFormatter
NSEnumerator
NSFileHandle
NSFormatter
NSHashTable 函數(shù)
NSInvocation
NSJavaSetup 函數(shù)
NSMapTable 函數(shù)
NSMutableArray
NSMutableAttributedString
NSMutableCharacterSet
NSMutableData
NSMutableDictionary
NSMutableSet
NSMutableString
NSNotificationQueue
NSNumberFormatter
NSPipe
NSPort
NSProcessInfo
NSRunLoop
NSScanner
NSSerializer
NSTask
NSUnarchiver
NSUndoManager
User name 和 home directory 函數(shù)
請(qǐng)注意,盡管NSSerializer、NSArchiver、NSCoder及NSEnumerator對(duì)象自身都是線程安全的,它們被列入這里的原因是當(dāng)它們包裹的數(shù)據(jù)對(duì)象被修改時(shí)是不安全的。比如,在使用歸檔的情況下,改變已歸檔的對(duì)象圖是不安全的。對(duì)于枚舉器,任何線程修改枚舉集合是不安全的。
只能在主線程中使用的類
以下類必須僅從應(yīng)用程序的主線程中使用。
NSAppleScript
可變 VS 不可變
不可變對(duì)象通常是線程安全的;一旦完成對(duì)其創(chuàng)建,你可以在線程間安全地傳遞這些對(duì)象。當(dāng)然,當(dāng)使用不可變對(duì)象時(shí),你仍需要記住引用計(jì)數(shù)的正確使用。如果你不正確地釋放不想保留的對(duì)象,隨后也會(huì)造成異常。
可變對(duì)象通常是非線程安全的。為在多線程應(yīng)用中使用可變對(duì)象,應(yīng)用必須使用鎖技術(shù)同步地訪問它們。總之,集合類型(如NSMutableArray,NSMutableDictionary)是非線程安全的。也就是說,如果一個(gè)或多個(gè)線程正在修改同一個(gè)數(shù)組,你必須在其讀寫區(qū)域上鎖以確保線程安全。
即便某一個(gè)方法聲明返回一個(gè)不可變對(duì)象,你絕不應(yīng)該簡(jiǎn)單地假設(shè)返回的對(duì)象是不可變的。取決于該方法的實(shí)現(xiàn),返回的對(duì)象可能是可變的也有可能是不可變的。例如,一個(gè)本該返回NSString的方法由于其實(shí)現(xiàn),可能事實(shí)上返回了一個(gè)NSMutableString。如果你想保證對(duì)象是不可變的,則必須創(chuàng)建一個(gè)不可變的備份。
可重入
TODO
類的初始化
Objective-C的運(yùn)行時(shí)系統(tǒng)會(huì)在類接收其他消息前向其發(fā)送initialize消息。這將使類在使用前有機(jī)會(huì)設(shè)置其運(yùn)行時(shí)環(huán)境。在多線程應(yīng)用中,運(yùn)行時(shí)保證只有一個(gè)線程-這個(gè)線程恰好向類發(fā)送第一條消息,即執(zhí)行initialize方法。如果當(dāng)?shù)谝粋€(gè)線程已經(jīng)進(jìn)入了initialize方法而第二個(gè)線程試圖向該類放松消息時(shí),第二個(gè)線程會(huì)阻塞直到initialize方法完成執(zhí)行。同時(shí),第一個(gè)線程可以繼續(xù)調(diào)用該類的其他方法。initialize方法不應(yīng)該由第二個(gè)線程調(diào)用;如果這樣做了,兩個(gè)線程會(huì)死鎖。
由于OS X 10.1.x及其早期版本存在的一個(gè)bug,線程能夠在其他線程執(zhí)行完initialize方法前向類發(fā)送消息。這樣一來線程會(huì)訪問到并未完全初始化好的值,并可能使應(yīng)用崩潰。如果你遇到這樣的問題,你需要引入鎖來阻止值的訪問直到它們完全地被初始化或者在類變成多線程操作前強(qiáng)制類初始化自身。
自動(dòng)釋放池
每個(gè)線程都維護(hù)著自己的NSAutoreleasePool對(duì)象棧。Cocoa認(rèn)為當(dāng)前線程的堆棧中總是有一個(gè)可用的自動(dòng)釋放池。如果一個(gè)池不可用,對(duì)象不被釋放并導(dǎo)致內(nèi)存泄漏。在基于Application Kit的應(yīng)用主線程中,其NSAutoreleasePool對(duì)象會(huì)自動(dòng)創(chuàng)建和銷毀,但輔助線程(和僅使用Foundation的應(yīng)用)在使用Cocoa前必須自己創(chuàng)建。如果你的線程是長(zhǎng)期運(yùn)行的且潛在地生成了大量的自動(dòng)釋放對(duì)象,你應(yīng)該周期性地銷毀和創(chuàng)建自動(dòng)釋放池(如Application Kit在主線程中所做一樣);否則,自動(dòng)釋放對(duì)象的積累并導(dǎo)致內(nèi)存的增長(zhǎng)。如果你的分離線程不使用Cocoa,則不需要?jiǎng)?chuàng)建一個(gè)自動(dòng)釋放池。
Run Loops
每個(gè)線程有且僅有一個(gè)run loop。每個(gè)run loop,都有自己的一系列模式來決定哪一個(gè)輸入源被監(jiān)聽。Run loop中定義的模式不受其他run loop模式的影響,即便它們有相同的名稱。
如果你的應(yīng)用基于Application Kit主線程的run loop將自動(dòng)運(yùn)行,但是輔助線程(和僅使用Foundation的應(yīng)用)必須自己?jiǎn)?dòng)run loop。如果分離不進(jìn)入run loop,在其方法執(zhí)行完畢后線程會(huì)立即退出。
雖然出于一些外部因素,NSRunLoop類并不是線程安全的,你只應(yīng)該從持有它的線程中使用該類的實(shí)例方法。
Application Kit 框架線程安全
下面部分描述了Application Kit框架中常用的線程安全內(nèi)容。
非線程安全類
下列的類和函數(shù)通常是非線程安全你的。在大多數(shù)情況下,你可以在多線程環(huán)境下使用它們,僅當(dāng)同一時(shí)刻同一線程時(shí)。
- NSGraphicsContext。
- NSImage。
- NSResponder。
- NSWindow及其所有的子類。
只能在主線程中使用的類
下列的類只能用于應(yīng)用的主線程中。
- NSCell及其子類。
- NSView及其子類。
窗口限制
你可以在輔助線程上創(chuàng)建窗口。Application Kit可以確保與窗口關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)在主線程上被銷毀以防止競(jìng)態(tài)情況發(fā)生。如果應(yīng)用程序同時(shí)處理大量的窗口,也存在窗口對(duì)象內(nèi)存泄漏的可能。
你可以在輔助線程上創(chuàng)建一個(gè)模態(tài)的窗口。當(dāng)主線程運(yùn)行在run loop的模態(tài)模式下時(shí),Application Kit會(huì)阻塞輔助線程的調(diào)用。
事件處理限制
應(yīng)用的主線程負(fù)責(zé)處理事件。主線程被NSApplication的run方法調(diào)用時(shí)阻塞,在應(yīng)用的main函數(shù)中調(diào)用。雖然其他線程進(jìn)入事件路徑時(shí)Application Kit將繼續(xù)工作,但它的操作可發(fā)生在事件隊(duì)列之外。例如,如果兩個(gè)線程同時(shí)響應(yīng)一個(gè)關(guān)鍵事件,事件會(huì)被亂序接收。讓主線程處理事件,可以帶來一致性的用戶體驗(yàn)。一旦收到事件,事件會(huì)被分發(fā)到輔助線程以供后續(xù)處理。
你可以從輔助線程調(diào)用NSApplication的postEvent:atStart:方法來向主線程的事件隊(duì)列推送事件。然而,由于用戶輸入的事件不同順序并不能夠得到保障。應(yīng)用的主線程仍會(huì)負(fù)責(zé)處理事件隊(duì)列中的事件。
圖形繪制限制
Application Kit中使用圖形相關(guān)的類和函數(shù)繪圖通常是線程安全的,包括NSBezierPath和NSString類。使用特定類的細(xì)節(jié)在下面部分將會(huì)描述。
- NSView限制
TODO - NSGraphicsContext限制
TODO - NSImage限制
TODO
Core Data 框架線程安全
Core Data框架支持多線程,盡管其中有些注意事項(xiàng)。獲取更多相關(guān)注意事項(xiàng),請(qǐng)查看《Core Data Programming Guide》。
Core Foundation
Core Foundation足夠的線程安全,如果程序中加以小心,你應(yīng)該不會(huì)陷入任何線程沖突的問題。通常情況下它都是線程安全的,比如說查詢、保留、釋放或者傳遞不可變的對(duì)象。即便多個(gè)線程對(duì)共享的對(duì)象進(jìn)行請(qǐng)求,它都是可靠的線程安全。
類似Cocoa,Core Foundation遭遇對(duì)象及對(duì)象內(nèi)部的變化時(shí)變得線程不安全的情況。例如,正如你所預(yù)期的那樣,修改可變數(shù)據(jù)或可變數(shù)組對(duì)象就是非線程安全的,修改不可變數(shù)組中的對(duì)象時(shí)也是如此。出于性能因素考慮,在這些情況下是至關(guān)重要的。此外,在該層級(jí)通常是不可能實(shí)現(xiàn)絕對(duì)的線程安全。你不能排除,如保持一個(gè)從集合中獲得的對(duì)象導(dǎo)致的不確定行為。集合本身可以在調(diào)用保留所包含的對(duì)象之前被釋放。
在這些情況下,從多線程中訪問Core Foundation對(duì)象,你的代碼應(yīng)該防止以鎖的方式同時(shí)訪問。例如,代碼枚舉了一個(gè)Core Foundation數(shù)組中的對(duì)象,應(yīng)使用適當(dāng)?shù)逆i定調(diào)用該枚舉塊以防止數(shù)組被其他線程改變。