由于文章長度限制呻纹,本文作為[譯]線程編程指南(二)后續(xù)部分。
線程安全技巧
同步工具是保證代碼線程安全的有效方式召嘶,但它不是萬能藥鹤耍。使用太多鎖或者其他類型的同步原語實際上會導(dǎo)致應(yīng)用多線程的性能反而不如非多線程時的性能。找到安全與性能之間的平衡點是一門需要經(jīng)驗的藝術(shù)苟穆。下列章節(jié)將為你的應(yīng)用選擇合適的同步等級提供幫助建議抄课。
避免同步
對于你工作的任何新項目,甚至對現(xiàn)有的項目雳旅,設(shè)計代碼和數(shù)據(jù)結(jié)構(gòu)來避免同步使用可能是最好的解決方案跟磨。雖然鎖和其他同步工具都很有用,但它們確實會影響任何應(yīng)用程序的性能攒盈。如果總體設(shè)計會導(dǎo)致特定資源之間的高度競爭抵拘,你的線程甚至?xí)却L的時間。
實現(xiàn)并發(fā)的最佳方法是減少并發(fā)任務(wù)之間的交互和相互依賴關(guān)系型豁。如果每個任務(wù)都在它自己的私有數(shù)據(jù)集上運行僵蛛,則不需要使用鎖來保護數(shù)據(jù)尚蝌。即使在兩個任務(wù)共享一個共同的數(shù)據(jù)集的情況下,你也可以為每個任務(wù)提供自己的備份充尉。當(dāng)然飘言,復(fù)制數(shù)據(jù)集也有它的成本,所以你在作出決定之前必須權(quán)衡這些成本和同步的成本驼侠。
理解同步的局限性
同步工具只有在使用多線程的應(yīng)用中才會有效姿鸿。如果你創(chuàng)建了一個互斥鎖來限制某個特定資源的訪問,所有的線程必須在嘗試操作該資源前請求這個鎖泪电。如果不這樣做般妙,提供這樣的互斥會另人困惑并成為程序猿的錯誤纪铺。
注意代碼正確性
當(dāng)使用鎖技術(shù)和內(nèi)存屏障技術(shù)時相速,你總是應(yīng)該更加小心地在代碼中為其提供位置。即使鎖看起來實際上可以讓你產(chǎn)生一種虛假的安全感鲜锚。下面的例子將會說明這個問題突诬,并指出在看似無害的代碼中的缺陷∥叻保基本的前提是旺隙,你有一個可變數(shù)組包含一組不變的對象。假設(shè)你想調(diào)用數(shù)組中的第一個對象的方法骏令。你可以使用下面的代碼:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
[anObject doSomething];
由于數(shù)組是可變的蔬捷,保護數(shù)組的鎖阻止了其他線程對于數(shù)組的修改直到你完成了從數(shù)組中獲取到想要的對象。同時因為你獲取到的對象是不可變的榔袋,所以鎖就沒有必要對調(diào)用doSomething
方法部分的代碼進行保護周拐。
盡管在前面的例子中存在這樣一個問題。如果釋放鎖時另一個線程來移除數(shù)組中的所有對象怔锌,你有機會在這之前執(zhí)行doSomething方法哲身?在一個沒有垃圾收集機制的應(yīng)用中笋婿,代碼中持有的對象可能被釋放,留下一個指向無效內(nèi)存地址的指針勾给。要解決這個問題,你可以簡單地重新安排你的現(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)用時對象仍然有效。不幸的是售睹,如果doSomething
方法需要花費很長時間來執(zhí)行旅择,這將導(dǎo)致代碼長時間的持有鎖,并造成性能上的瓶頸侣姆。
這段代碼的問題不是臨界區(qū)定義得不好生真,而真正的問題并沒有理解沉噩。真正的問題是由其他線程的存在而觸發(fā)的內(nèi)存管理問題。由于對象能夠被其他線程釋放柱蟀,所以更好的解決辦法是在鎖釋放之前持有anObject
川蒙。該解決方案解決了對象被釋放的實際問題,并沒有引入一個潛在的性能隱患长已。
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
[anObject doSomething];
[anObject release];
盡管先前的示例事實上非常簡單畜眨,它們確實說明了非常重要的一點。說到正確性术瓮,你必須考慮到這個明顯的問題康聂。內(nèi)存管理和其他方面的設(shè)計也可能會受到多線程存在的影響,所以你要提前考慮這些問題胞四。此外恬汁,當(dāng)涉及到安全問題時,你應(yīng)該經(jīng)常假設(shè)編譯器可能會做最壞的事情辜伟。這種意識和警惕性應(yīng)該幫助你避免潛在的問題氓侧,確保代碼的行為正確。
獲取更多線程安全的示例导狡,請看之后的線程安全總結(jié)约巷。
當(dāng)心死鎖與活鎖
任何時候一個線程試圖同時持有一個以上的鎖時,有可能發(fā)生死鎖旱捧。死鎖發(fā)生在兩個不同的線程各自持有一個鎖独郎,而線程同時需要持有對方所持有的鎖時。其結(jié)果是枚赡,每個線程都會永久地被阻塞氓癌,因為它永遠無法獲得另一個鎖。
活鎖和死鎖類似标锄,同樣發(fā)生在兩個線程競爭同一資源時顽铸。在活鎖的情況中,一個線程放棄其鎖并試圖獲取另一個鎖料皇。一旦它獲得了另一個鎖谓松,它又返回并試圖獲得第一個鎖。這樣一來它會被鎖住践剂,因為它花費了所有時間來釋放一個鎖并試圖獲得另一個鎖鬼譬,而不是做任何實際工作。
為了同時避免死鎖和活鎖的情況逊脯,最好的辦法是一次只拿一個鎖优质。如果你必須同時獲得一個以上的鎖,你應(yīng)該確保其他線程不嘗試做相同的事情。
正確使用volatile變量
如果你已經(jīng)使用互斥鎖來保護一段代碼巩螃,不要想當(dāng)然地認為你需要使用volatile關(guān)鍵字來保護該區(qū)域的重要變量演怎。互斥包括了一個確保已載入和已存儲操作正確順序的內(nèi)存屏障避乏。在臨界區(qū)內(nèi)將變量強制設(shè)置為volatile可以保證每次獲取的值都是來自于內(nèi)存當(dāng)中爷耀。在特定情況下這兩種技術(shù)結(jié)合使用也許是必要的,但也導(dǎo)致顯著的性能損失拍皮。如果單獨使用互斥足以保護變量歹叮,請省略使用關(guān)鍵字volatile。
同樣重要的是铆帽,在不使用互斥的時候也不必使用volatile變量咆耿。總的來說爹橱,互斥和其他同步機制是比volatile更好的保護數(shù)據(jù)結(jié)構(gòu)完整性的方式萨螺。Volatile關(guān)鍵字只確保一個變量是從內(nèi)存中而不是寄存器中加載,并不能確保你的代碼可以正確地訪問該變量宅荤。
使用原子操作
非阻塞式的同步是一種可以執(zhí)行某些操作并避免鎖消耗的方式屑迂。雖然鎖是兩個線程間一種有效的同步方式浸策,但請求鎖是資源消耗相對昂貴的操作冯键,即便是在非沖突情況下。相反的庸汗,許多原子性操作只占用小部分時間來完成和鎖同樣有效的操作惫确。
原子操作讓你在32位或64位值上執(zhí)行簡單的數(shù)學(xué)和邏輯運算。這些操作依賴于特殊的硬件指令(和可選的內(nèi)存屏障)蚯舱,以確保在相關(guān)的內(nèi)存再次訪問之前完成既定操作改化。在多線程的情況下,你應(yīng)該經(jīng)常使用包含內(nèi)存障礙的原子操作以確保這部分存儲在線程之間是正確同步的枉昏。
表4-3列舉了可用的原子性數(shù)學(xué)和邏輯操作以及相應(yīng)的函數(shù)名稱陈肛。這些函數(shù)全部聲明在/usr/include/libkern/OSAtomic.h頭文件中,你可以在里面找到完整的語法兄裂。這些函數(shù)的64位版本只存在與64位的進程中句旱。
表4-3 原子性的數(shù)學(xué)和邏輯操作
操作 | 函數(shù)名稱 | 描述 |
---|---|---|
加(Add) | OSAtomicAdd32 OSAtomicAdd32Barrier OSAtomicAdd64 OSAtomicAdd64Barrier |
兩個整型值相加并將結(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 |
對變量的舊值進行比較。如果兩個值是相等的,這個函數(shù)將指定新值賦給該變量溯乒;否則夹厌,它什么也不做。比較和賦值作為一個原子操作裆悄,該函數(shù)會返回一個布爾值以表示是否發(fā)生交換尊流。 |
測試和設(shè)置(Test and set) | OSAtomicTestAndSet OSAtomicTestAndSetBarrier |
在指定的變量中測試一個位,將該位設(shè)置為1灯帮,并將老位的值作為布爾值返回崖技。位根據(jù)公式進行測試(0x80 >> (n & 7))字節(jié)((char*)address + (n >> 3)),n是位號碼和地址是一個指針變量钟哥。這個公式有效地分解成8位大小的塊迎献,并在每一個塊中的位順序反轉(zhuǎn)。例如腻贰,為了測試一個32位整數(shù)的最低序位(位0)吁恍,你將實際指定的位號為7;同樣播演,要測試的最高點位(位32)冀瓦,你將指定24位數(shù)字。 |
測試和清理(Test and clear) | OSAtomicTestAndClear OSAtomicTestAndClearBarrier |
在指定的變量中測試一個位写烤,將該位設(shè)置為0翼闽,并將老位的值返回布爾值。位根據(jù)公式進行測試(0x80 >> (n & 7))字節(jié)((char*)address + (n >> 3))洲炊,n是位號碼和地址是一個指針變量感局。這個公式有效地分解成8位大小的塊,并在每一個塊中的位順序反轉(zhuǎn)暂衡。例如询微,為了測試一個32位整數(shù)的最低序位(位0),你將實際指定的位號為7狂巢;同樣撑毛,要測試的最高點位(位32),你將指定24位數(shù)字唧领。 |
大多數(shù)原子函數(shù)的行為應(yīng)該是相對簡單并如你所期望的藻雌。然而代碼4-1,顯示了原子性的test-and-set以及compare-and-swap操作相對復(fù)雜的行為疹吃。前面三個調(diào)用OSAtomicTestAndSet
函數(shù)來展示位操作公式如何被用于整型值蹦疑,并且其結(jié)果可能與你所期望的不同。后面兩個調(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)原子性操作的信息,請查看atomic的man幫助頁或者/usr/include/libkern/OSAtomic.h頭文件叁温。
使用鎖
鎖作為線程編程的基本同步工具再悼。鎖使你能夠很容易地保護大段代碼,這樣你就可以確保代碼的正確性膝但。OS X和iOS為所有應(yīng)用類型提供了基本的互斥鎖冲九,并且Foundation Framework為特殊的情形定義了額外的變量。下面的章節(jié)將向你展示如何使用這些鎖類型跟束。
使用POSIX的Mutex鎖
POSIX的互斥鎖在任何應(yīng)用中都能夠極其簡單地使用莺奸。為創(chuàng)建互斥鎖,你需要聲明并初始化一個pthread_mutex_t
結(jié)構(gòu)體冀宴。為完成鎖和解鎖的操作灭贷,你需要使用pthread_mutex_lock
和pthread_mutex_unlock
函數(shù)。代碼4-2展示了使用POSIX線程互斥鎖所需要初始化的基本代碼略贮。當(dāng)你完成了該鎖的操作時甚疟,簡單地調(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);
}
注意:以上代碼只是一個展示POSIX線程互斥鎖函數(shù)的簡單示例逃延。你自己的代碼必須檢查這些函數(shù)返回的錯誤碼并正確地處理它們览妖。
使用NSLock
NSLock對象為Cocoa應(yīng)用實現(xiàn)了基本的互斥功能。所有鎖(包括NSLock)事實上由NSLocking協(xié)議定義揽祥,該協(xié)議同樣定義了lock
和unlock
方法讽膏。你可以在任何需要互斥的地方使用這些方法來請求鎖以及釋放鎖。
除了標(biāo)準(zhǔn)的鎖操作之外盔然,NSLock類還加入了tryLock
和lockBeforeDate:
方法桅打。tryLock
方法試圖獲取鎖但在所不可用時并不阻塞線程是嗜,而是返回NO愈案。lockBeforeDate:
方法在指定時間內(nèi)鎖不能獲取時試圖獲取鎖但不阻塞線程(并返回NO)。
下面的示例將向你展示如何使用NSLock來調(diào)節(jié)可視化視圖的更新鹅搪,視圖更新的數(shù)據(jù)來自于其他線程的計算結(jié)果站绪。如果線程不能立即請求到鎖,它會繼續(xù)其計算操作直到所能夠獲取時更新顯示丽柿。
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)該做的事情-它防止不同線程在同一時間請求相同的鎖。在這種情況下甫题,你沒有必要創(chuàng)建互斥鎖或者直接鎖住一個對象馁筐。相反地,你可以簡單地使用任何Objective-C對象作為鎖令牌坠非,正如下面代碼所示:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
傳遞給@synchronized語句的對象會成為區(qū)別受保護代碼塊的唯一標(biāo)識敏沉。如果你在兩個線程中執(zhí)行先前的這個方法,并向每個線程傳遞不同的對象作為anObj參數(shù),每個線程會獲得這個鎖并不受阻塞的繼續(xù)執(zhí)行盟迟。如果你同時傳遞同一個對象秋泳,其中一個線程會首先獲得鎖并使得另一個線程阻塞直到第一個線程退出了臨界區(qū)。
作為一種預(yù)防措施攒菠,@synchronized塊會向受到保護的代碼隱式地添加異常處理回調(diào)迫皱。該回調(diào)在異常拋出時會自動釋放互斥鎖。這意味著為了使用@synchronized語句辖众,你必須在代碼中開啟Objective-C的異常處理卓起。如果你不希望由隱式異常處理程序引起額外的開銷,你應(yīng)該考慮使用鎖類凹炸。
使用其他的Cocoa鎖
下面的章節(jié)將描述Cocoa其他類型鎖的使用既绩。
使用NSRecursiveLock對象
NSRecursiveLock類定義了一種可以多次被同一線程請求且不導(dǎo)致線程死鎖的鎖類型。遞歸鎖必須記錄有好多次被成功請求还惠。每次鎖的成功請求必須由相應(yīng)的次數(shù)的鎖和解鎖調(diào)用來平衡饲握。只有當(dāng)所有的鎖和解鎖調(diào)用平衡時鎖才會被釋放并繼續(xù)有其他線程請求。
正如其名字暗示的一樣蚕键,該類型的鎖通常用于遞歸函數(shù)來防止遞歸操作導(dǎo)致線程的阻塞救欧。在非遞歸的情況下,你可以用它來調(diào)用那些語意上仍希望持有鎖的函數(shù)锣光。下面是一個遞歸函數(shù)中請求該鎖的代碼示例笆怠。如果你不像代碼中那樣使用NSRecursiveLock對象,你的線程在函數(shù)再次調(diào)用時產(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)用平衡時才釋放蹬刷,你應(yīng)該仔細權(quán)衡使用鎖和這樣做造成潛在的性能影響。在一個較長的時間內(nèi)保持任何鎖會使其它線程阻塞直到遞歸完成频丘。如果可以重寫代碼來消除遞歸或需要使用的遞歸鎖办成,則可以實現(xiàn)更好的性能。
使用NSConditionLock對象
NSConditionLock類定義了可以根據(jù)特殊值來進行鎖和解鎖操作的互斥鎖搂漠。你不應(yīng)該將該類型的鎖和之前的條件量混為一談迂卢。它和條件量某種意義上講行為相似,但實現(xiàn)方式完全不同桐汤。
通常而克,你將NSConditionLock對象用于線程需要執(zhí)行特定順序的任務(wù)時,比如一個線程生產(chǎn)數(shù)據(jù)而另一個線程消費數(shù)據(jù)怔毛。當(dāng)生產(chǎn)者執(zhí)行時员萍,消費者請求鎖的條件取決于你的程序。(條件本身僅僅是一個定義的整型值)當(dāng)生產(chǎn)者完成時拣度,它會解鎖并將鎖條件置為合適的整型值來喚醒消費者線程碎绎,消費者線程然后收到并處理數(shù)據(jù)蜂莉。
NSConditionLock對象中的鎖定和解鎖方法可以任意地組合使用。例如混卵,你可以將鎖定信息配對給unlockWithCondition:
映穗,或者解鎖信息配對給lockWithCondition:
。當(dāng)然幕随,這一組合解鎖但不會釋放任何線程等待特定的條件值蚁滋。
下面的示例演示了如何使用條件鎖處理“生產(chǎn)者-消費者”問題。設(shè)想應(yīng)用程序包含一個數(shù)據(jù)隊列赘淮。生產(chǎn)者線程將數(shù)據(jù)添加到隊列辕录,而消費者線程從隊列中提取數(shù)據(jù)。生產(chǎn)者不需要等待一個特定的條件梢卸,但它必須等待鎖以便它可以安全地添加數(shù)據(jù)到隊列走诞。
id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
while(true)
{
[condLock lock];
/* Add data to the queue. */
[condLock unlockWithCondition:HAS_DATA];
}
因為鎖的初始條件設(shè)置為NO_DATA,所以生產(chǎn)者線程期初獲取鎖并不受影響蛤高。它將隊列填充好數(shù)據(jù)并將條件設(shè)置為HAS_DATA蚣旱。在隨后的迭代中,生產(chǎn)者線程可以在到達時添加新的數(shù)據(jù)不管隊列是否是空的還是有一些數(shù)據(jù)戴陡。當(dāng)消費者線程從隊列中提取數(shù)據(jù)時塞绿,它阻塞的唯一時間是消費者線程從隊列中提取數(shù)據(jù)時。
因為消費者線程必須要有數(shù)據(jù)處理恤批,它根據(jù)特定的條件等待隊列异吻。當(dāng)生產(chǎn)者將數(shù)據(jù)放在隊列上時,消費者線程喚醒并請求鎖喜庞。然后诀浪,它可以從隊列中提取一些數(shù)據(jù)并更新隊列狀態(tà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對象
NSDistributedLock類可用于多個宿主機上的多個應(yīng)用之間來限制某些共享資源的訪問雷猪,例如文件。該鎖本身是一種由文件系統(tǒng)(如文件或者目錄)實現(xiàn)的非常高效的互斥鎖窄潭。為使NSDistributedLock對象可用春宣,該鎖必須由所有的應(yīng)用來使用。這通常意味著把它放在所有計算機上的應(yīng)用程序都可以訪問的文件系統(tǒng)中嫉你。
不像其他類型的鎖,NSDistributedLock并不遵循NSLocking協(xié)議并且沒有lock
方法躏惋。lock
方法會阻塞線程的執(zhí)行幽污,并要求系統(tǒng)以一個預(yù)定的速率輪詢鎖。NSDistributedLock提供tryLock
方法讓你決定是否輪詢簿姨,而不是在你自己的代碼中這樣做距误。
由于它使用文件系統(tǒng)來實現(xiàn)簸搞,NSDistributedLock對象直到在持有者顯式地釋放它時釋放。如果你的應(yīng)用在持有分布式鎖是崩潰了准潭,其他的客戶端將不能對保護資源進行訪問趁俊。在這種情況下,你可以使用breakLock
方法來打破既存鎖以便你能夠請求到它刑然。破壞鎖通常是需要避免的寺擂,除非你確定鎖的持有者死掉了且不能釋放鎖。
同其他類型的鎖一樣泼掠,當(dāng)你用完NSDistributedLock對象后怔软,可以使用unlock
方法來釋放它。
使用條件量
條件量是一種用于同步操作順序的特殊類型的鎖择镇。它與互斥鎖之間只有細微的差別挡逼。線程會保持阻塞直到其他線程顯式地喚醒條件量。
由于細節(jié)涉及到操作系統(tǒng)實現(xiàn)腻豌,條件量允許假定還鎖成功家坎,即使沒有在代碼中喚醒它們。為了避免這些虛假信號引起的問題吝梅,你應(yīng)該經(jīng)常使用一個謂詞與你的條件量一起使用乘盖。謂詞是一個更具體的方法,它決定是否安全地為你的線程進行處理憔涉。條件量簡單地保持你的線程睡眠订框,直到謂詞可以被喚醒線程設(shè)置。
下面的章節(jié)將告訴你如何在代碼中使用條件量兜叨。
使用NSCondition
NSCondition類提供了和POSIX條件量語意相同穿扳,但同時包裝了鎖和條件數(shù)據(jù)到單個對象的數(shù)據(jù)結(jié)構(gòu)。這就使得對象可以像互斥鎖并且像條件量那樣等待條件国旷。
代碼4-3代碼段展示了為等待NSCondition對象的事件隊列矛物。cocoaCondition
變量包含一個NSCondition對象和timeToDoWork
,由其他線程喚醒條件時自增的整型變量跪但。
代碼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ù)據(jù)結(jié)構(gòu)和互斥鎖的功能屡久。盡管兩個鎖結(jié)構(gòu)各自獨立忆首,但在運行時互斥鎖緊密地綁定著條件結(jié)構(gòu)。等待一個信號的線程應(yīng)該總是一起使用這樣非互斥鎖和條件結(jié)構(gòu)被环。改變這樣的配對可能造成錯誤糙及。
代碼4-5展示了條件量和謂詞的基本初始化和使用。經(jīng)過初始化的條件量和互斥鎖筛欢,等待線程使用ready_to_go
變量作為謂詞并進入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ā)信號線程負責(zé)設(shè)置謂詞柱搜,并將信號發(fā)送到條件量迟郎。代碼4-6顯示了實現(xiàn)該行為的代碼。在這個例子中聪蘸,條件量是在互斥內(nèi)部被喚醒以防止等待條件的線程間的競態(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);
}
注意:以上代碼只是一個展示POSIX線程條件量函數(shù)的簡單示例。你自己的代碼必須檢查這些函數(shù)返回的錯誤碼并正確地處理它們宇姚。
附錄A:線程安全總結(jié)
本附錄描述了OS X和iOS中某些關(guān)鍵框架的高級別的線程安全匈庭。本附錄中的信息是隨時變更的。
Cocoa
多線程中使用Cocoa的指導(dǎo)如下:
- 不可變(immutable)對象通常是線程安全的浑劳。一旦你創(chuàng)建它們阱持,你可以在線程間安全地傳遞這些對象。另一方面魔熏,可變(mutable)對象通常不是線程安全的衷咽。在多線程應(yīng)用中使用可變對象,應(yīng)用程序必須正確同步蒜绽。
- 許多對象看似“安全”實則在多線程中使用時不安全镶骗。許多這些對象可以在任何線程中使用,只要在同一時間且同一線程躲雅。被嚴(yán)格限制在應(yīng)用程序的主線程上的對象被調(diào)用時就是如此鼎姊。
- 應(yīng)用程序的主線程負責(zé)處理事件。雖然其他線程進入事件路徑時Application Kit將繼續(xù)工作相赁,但它的操作可發(fā)生在事件隊列之外相寇。
- 如果你想用線程來繪制視圖,使用NSView的
lockFocusIfCanDraw
和unlockFocus
方法將所有繪制代碼包括進來钮科。 - 為了能在Cocoa中使用POSIX線程唤衫,你必須首先將應(yīng)用置于多線程模式。
Foundation Framework線程安全
有一種誤解绵脯,認為Foundation Framework是線程安全的佳励,而Application Kit不是線程安全的。不幸的是蛆挫,這只是一個總的概括赃承,有些誤導(dǎo)。每個框架都有線程安全的區(qū)域和線程不安全的區(qū)域璃吧。下面的章節(jié)描述了Foundation Framework的通用的線程安全性楣导。
線程安全的類和函數(shù)
下列的類和函數(shù)通常被認為是線程安全的。你可以在多個線程中使用相同實例而不需請求鎖畜挨。
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
對象的allocation 和 retain count 函數(shù)
Zone 和 memory 函數(shù)
非線程安全的類和函數(shù)
下列的類和函數(shù)通常被認為是非線程安全的筒繁。大多數(shù)情況下,你可以在多線程環(huán)境使用這些類只要你在同一時刻同一線程中巴元。
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ù)
請注意毡咏,盡管NSSerializer
、NSArchiver
逮刨、NSCoder
及NSEnumerator
對象自身都是線程安全的呕缭,它們被列入這里的原因是當(dāng)它們包裹的數(shù)據(jù)對象被修改時是不安全的。比如修己,在使用歸檔的情況下恢总,改變已歸檔的對象圖是不安全的。對于枚舉器睬愤,任何線程修改枚舉集合是不安全的片仿。
只能在主線程中使用的類
以下類必須僅從應(yīng)用程序的主線程中使用。
NSAppleScript
可變 VS 不可變
不可變對象通常是線程安全的尤辱;一旦完成對其創(chuàng)建砂豌,你可以在線程間安全地傳遞這些對象。當(dāng)然光督,當(dāng)使用不可變對象時阳距,你仍需要記住引用計數(shù)的正確使用。如果你不正確地釋放不想保留的對象结借,隨后也會造成異常筐摘。
可變對象通常是非線程安全的。為在多線程應(yīng)用中使用可變對象船老,應(yīng)用必須使用鎖技術(shù)同步地訪問它們咖熟。總之,集合類型(如NSMutableArray努隙,NSMutableDictionary)是非線程安全的球恤。也就是說,如果一個或多個線程正在修改同一個數(shù)組荸镊,你必須在其讀寫區(qū)域上鎖以確保線程安全咽斧。
即便某一個方法聲明返回一個不可變對象,你絕不應(yīng)該簡單地假設(shè)返回的對象是不可變的躬存。取決于該方法的實現(xiàn)张惹,返回的對象可能是可變的也有可能是不可變的。例如岭洲,一個本該返回NSString的方法由于其實現(xiàn)宛逗,可能事實上返回了一個NSMutableString。如果你想保證對象是不可變的盾剩,則必須創(chuàng)建一個不可變的備份雷激。
可重入
TODO
類的初始化
Objective-C的運行時系統(tǒng)會在類接收其他消息前向其發(fā)送initialize
消息替蔬。這將使類在使用前有機會設(shè)置其運行時環(huán)境。在多線程應(yīng)用中屎暇,運行時保證只有一個線程-這個線程恰好向類發(fā)送第一條消息承桥,即執(zhí)行initialize
方法。如果當(dāng)?shù)谝粋€線程已經(jīng)進入了initialize
方法而第二個線程試圖向該類放松消息時根悼,第二個線程會阻塞直到initialize
方法完成執(zhí)行凶异。同時,第一個線程可以繼續(xù)調(diào)用該類的其他方法挤巡。initialize
方法不應(yīng)該由第二個線程調(diào)用剩彬;如果這樣做了,兩個線程會死鎖矿卑。
由于OS X 10.1.x及其早期版本存在的一個bug喉恋,線程能夠在其他線程執(zhí)行完initialize
方法前向類發(fā)送消息。這樣一來線程會訪問到并未完全初始化好的值粪摘,并可能使應(yīng)用崩潰瀑晒。如果你遇到這樣的問題,你需要引入鎖來阻止值的訪問直到它們完全地被初始化或者在類變成多線程操作前強制類初始化自身徘意。
自動釋放池
每個線程都維護著自己的NSAutoreleasePool對象棧苔悦。Cocoa認為當(dāng)前線程的堆棧中總是有一個可用的自動釋放池。如果一個池不可用椎咧,對象不被釋放并導(dǎo)致內(nèi)存泄漏玖详。在基于Application Kit的應(yīng)用主線程中,其NSAutoreleasePool對象會自動創(chuàng)建和銷毀勤讽,但輔助線程(和僅使用Foundation的應(yīng)用)在使用Cocoa前必須自己創(chuàng)建蟋座。如果你的線程是長期運行的且潛在地生成了大量的自動釋放對象,你應(yīng)該周期性地銷毀和創(chuàng)建自動釋放池(如Application Kit在主線程中所做一樣)脚牍;否則向臀,自動釋放對象的積累并導(dǎo)致內(nèi)存的增長。如果你的分離線程不使用Cocoa诸狭,則不需要創(chuàng)建一個自動釋放池券膀。
Run Loops
每個線程有且僅有一個run loop。每個run loop驯遇,都有自己的一系列模式來決定哪一個輸入源被監(jiān)聽芹彬。Run loop中定義的模式不受其他run loop模式的影響,即便它們有相同的名稱叉庐。
如果你的應(yīng)用基于Application Kit主線程的run loop將自動運行舒帮,但是輔助線程(和僅使用Foundation的應(yīng)用)必須自己啟動run loop。如果分離不進入run loop,在其方法執(zhí)行完畢后線程會立即退出玩郊。
雖然出于一些外部因素肢执,NSRunLoop類并不是線程安全的,你只應(yīng)該從持有它的線程中使用該類的實例方法瓦宜。
Application Kit 框架線程安全
下面部分描述了Application Kit框架中常用的線程安全內(nèi)容蔚万。
非線程安全類
下列的類和函數(shù)通常是非線程安全你的岭妖。在大多數(shù)情況下临庇,你可以在多線程環(huán)境下使用它們,僅當(dāng)同一時刻同一線程時昵慌。
- NSGraphicsContext假夺。
- NSImage。
- NSResponder斋攀。
- NSWindow及其所有的子類已卷。
只能在主線程中使用的類
下列的類只能用于應(yīng)用的主線程中。
- NSCell及其子類淳蔼。
- NSView及其子類侧蘸。
窗口限制
你可以在輔助線程上創(chuàng)建窗口。Application Kit可以確保與窗口關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)在主線程上被銷毀以防止競態(tài)情況發(fā)生鹉梨。如果應(yīng)用程序同時處理大量的窗口讳癌,也存在窗口對象內(nèi)存泄漏的可能。
你可以在輔助線程上創(chuàng)建一個模態(tài)的窗口存皂。當(dāng)主線程運行在run loop的模態(tài)模式下時晌坤,Application Kit會阻塞輔助線程的調(diào)用。
事件處理限制
應(yīng)用的主線程負責(zé)處理事件旦袋。主線程被NSApplication的run
方法調(diào)用時阻塞骤菠,在應(yīng)用的main
函數(shù)中調(diào)用。雖然其他線程進入事件路徑時Application Kit將繼續(xù)工作疤孕,但它的操作可發(fā)生在事件隊列之外商乎。例如,如果兩個線程同時響應(yīng)一個關(guān)鍵事件祭阀,事件會被亂序接收鹉戚。讓主線程處理事件,可以帶來一致性的用戶體驗柬讨。一旦收到事件崩瓤,事件會被分發(fā)到輔助線程以供后續(xù)處理。
你可以從輔助線程調(diào)用NSApplication的postEvent:atStart:
方法來向主線程的事件隊列推送事件踩官。然而却桶,由于用戶輸入的事件不同順序并不能夠得到保障。應(yīng)用的主線程仍會負責(zé)處理事件隊列中的事件。
圖形繪制限制
Application Kit中使用圖形相關(guān)的類和函數(shù)繪圖通常是線程安全的颖系,包括NSBezierPath和NSString類嗅剖。使用特定類的細節(jié)在下面部分將會描述。
- NSView限制
TODO - NSGraphicsContext限制
TODO - NSImage限制
TODO
Core Data 框架線程安全
Core Data框架支持多線程嘁扼,盡管其中有些注意事項信粮。獲取更多相關(guān)注意事項,請查看《Core Data Programming Guide》趁啸。
Core Foundation
Core Foundation足夠的線程安全强缘,如果程序中加以小心,你應(yīng)該不會陷入任何線程沖突的問題不傅。通常情況下它都是線程安全的旅掂,比如說查詢、保留访娶、釋放或者傳遞不可變的對象商虐。即便多個線程對共享的對象進行請求,它都是可靠的線程安全崖疤。
類似Cocoa秘车,Core Foundation遭遇對象及對象內(nèi)部的變化時變得線程不安全的情況。例如劫哼,正如你所預(yù)期的那樣叮趴,修改可變數(shù)據(jù)或可變數(shù)組對象就是非線程安全的,修改不可變數(shù)組中的對象時也是如此沦偎。出于性能因素考慮疫向,在這些情況下是至關(guān)重要的。此外,在該層級通常是不可能實現(xiàn)絕對的線程安全。你不能排除尉桩,如保持一個從集合中獲得的對象導(dǎo)致的不確定行為。集合本身可以在調(diào)用保留所包含的對象之前被釋放舌涨。
在這些情況下,從多線程中訪問Core Foundation對象扔字,你的代碼應(yīng)該防止以鎖的方式同時訪問囊嘉。例如,代碼枚舉了一個Core Foundation數(shù)組中的對象革为,應(yīng)使用適當(dāng)?shù)逆i定調(diào)用該枚舉塊以防止數(shù)組被其他線程改變扭粱。