這篇文章里,我們將會(huì)討論一些 iOS 和 OS X 都可以使用的底層 API。除了 dispatch_once ,我們一般不鼓勵(lì)使用其中的任何一種技術(shù)。
但是我們想要揭示出表面之下深層次的一些可利用的方面。這些底層的 API 提供了大量的靈活性,隨之而來的是大量的復(fù)雜度和更多的責(zé)任。在我們的文章常見的后臺(tái)實(shí)踐中提到的高層的 API 和模式能夠讓你專注于手頭的任務(wù)并且免于大量的問題。通常來說,高層的 API 會(huì)提供更好的性能,除非你能承受起使用底層 API 帶來的糾結(jié)于調(diào)試代碼的時(shí)間和努力。
盡管如此,了解深層次下的軟件堆棧工作原理還是有很有幫助的。我們希望這篇文章能夠讓你更好的了解這個(gè)平臺(tái),同時(shí),讓你更加感謝這些高層的 API。
首先,我們將會(huì)分析大多數(shù)組成 Grand Central Dispatch 的部分。它已經(jīng)存在了好幾年,并且蘋果公司持續(xù)添加功能并且改善它?,F(xiàn)在蘋果已經(jīng)將其開源,這意味著它對(duì)其他平臺(tái)也是可用的了。最后,我們將會(huì)看一下原子操作——另外的一種底層代碼塊的集合。
或許關(guān)于并發(fā)編程最好的書是 M. Ben-Ari 寫的《Principles of Concurrent Programming》,ISBN 0-13-701078-8。如果你正在做任何與并發(fā)編程有關(guān)的事情,你需要讀一下這本書。這本書已經(jīng)30多年了,仍然非常卓越。書中簡潔的寫法,優(yōu)秀的例子和練習(xí),帶你領(lǐng)略并發(fā)編程中代碼塊的基本原理。這本書現(xiàn)在已經(jīng)絕版了,但是它的一些復(fù)印版依然廣為流傳。有一個(gè)新版書,名字叫《Principles of Concurrent and Distributed Programming》,ISBN 0-321-31283-X,好像有很多相同的地方,不過我還沒有讀過。
或許GCD中使用最多并且被濫用功能的就是 dispatch_once 了。正確的用法看起來是這樣的:
+ (UIColor *)boringColor;
{
static UIColor *color;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
color = [UIColor colorWithRed:0.380f green:0.376f blue:0.376f alpha:1.000f];
});
return color;
}
上面的 block 只會(huì)運(yùn)行一次。并且在連續(xù)的調(diào)用中,這種檢查是很高效的。你能使用它來初始化全局?jǐn)?shù)據(jù)比如單例。要注意的是,使用 dispatch_once_t 會(huì)使得測(cè)試變得非常困難(單例和測(cè)試不是很好配合)。
要確保 onceToken 被聲明為 static ,或者有全局作用域。任何其他的情況都會(huì)導(dǎo)致無法預(yù)知的行為。換句話說,不要把 dispatch_once_t 作為一個(gè)對(duì)象的成員變量,或者類似的情形。
退回到遠(yuǎn)古時(shí)代(其實(shí)也就是幾年前),人們會(huì)使用 pthread_once ,因?yàn)?dispatch_once_t 更容易使用并且不易出錯(cuò),所以你永遠(yuǎn)都不會(huì)再用到 pthread_once 了。
另一個(gè)常見的小伙伴就是 dispatch_after 了。它使工作延后執(zhí)行。它是很強(qiáng)大的,但是要注意:你很容易就陷入到一堆麻煩中。一般用法是這樣的:
- (void)foo
{
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self bar];
});
}
第一眼看上去這段代碼是極好的。但是這里存在一些缺點(diǎn)。我們不能(直接)取消我們已經(jīng)提交到 dispatch_after 的代碼,它將會(huì)運(yùn)行。
另外一個(gè)需要注意的事情就是,當(dāng)人們使用 dispatch_after 去處理他們代碼中存在的時(shí)序 bug 時(shí),會(huì)存在一些有問題的傾向。一些代碼執(zhí)行的過早而你很可能不知道為什么會(huì)這樣,所以你把這段代碼放到了 dispatch_after 中,現(xiàn)在一切運(yùn)行正常了。但是幾周以后,之前的工作不起作用了。由于你并不十分清楚你自己代碼的執(zhí)行次序,調(diào)試代碼就變成了一場噩夢(mèng)。所以不要像上面這樣做。大多數(shù)的情況下,你最好把代碼放到正確的位置。如果代碼放到 -viewWillAppear 太早,那么或許 -viewDidAppear 就是正確的地方。
通過在自己代碼中建立直接調(diào)用(類似 -viewDidAppear )而不是依賴于 dispatch_after ,你會(huì)為自己省去很多麻煩。
如果你需要一些事情在某個(gè)特定的時(shí)刻運(yùn)行,那么 dispatch_after 或許會(huì)是個(gè)好的選擇。確保同時(shí)考慮了 NSTimer,這個(gè)API雖然有點(diǎn)笨重,但是它允許你取消定時(shí)器的觸發(fā)。
GCD 中一個(gè)基本的代碼塊就是隊(duì)列。下面我們會(huì)給出一些如何使用它的例子。當(dāng)使用隊(duì)列的時(shí)候,給它們一個(gè)明顯的標(biāo)簽會(huì)幫自己不少忙。在調(diào)試時(shí),這個(gè)標(biāo)簽會(huì)在 Xcode (和 lldb)中顯示,這會(huì)幫助你了解你的 app 是由什么決定的:
- (id)init;
{
self = [super init];
if (self != nil) {
NSString *label = [NSString stringWithFormat:@"%@.isolation.%p", [self class], self];
self.isolationQueue = dispatch_queue_create([label UTF8String], 0);
label = [NSString stringWithFormat:@"%@.work.%p", [self class], self];
self.workQueue = dispatch_queue_create([label UTF8String], 0);
}
return self;
}
隊(duì)列可以是并行也可以是串行的。默認(rèn)情況下,它們是串行的,也就是說,任何給定的時(shí)間內(nèi),只能有一個(gè)單獨(dú)的 block 運(yùn)行。這就是隔離隊(duì)列(原文:isolation queues。譯注)的運(yùn)行方式。隊(duì)列也可以是并行的,也就是同一時(shí)間內(nèi)允許多個(gè) block 一起執(zhí)行。
GCD 隊(duì)列的內(nèi)部使用的是線程。GCD 管理這些線程,并且使用 GCD 的時(shí)候,你不需要自己創(chuàng)建線程。但是重要的外在部分 GCD 會(huì)呈現(xiàn)給你,也就是用戶 API,一個(gè)很大不同的抽象層級(jí)。當(dāng)使用 GCD 來完成并發(fā)的工作時(shí),你不必考慮線程方面的問題,取而代之的,只需考慮隊(duì)列和功能點(diǎn)(提交給隊(duì)列的 block)。雖然往下深究,依然都是線程,但是 GCD 的抽象層級(jí)為你慣用的編碼提供了更好的方式。
隊(duì)列和功能點(diǎn)同時(shí)解決了一個(gè)連續(xù)不斷的扇出的問題:如果我們直接使用線程,并且想要做一些并發(fā)的事情,我們很可能將我們的工作分成 100 個(gè)小的功能點(diǎn),然后基于可用的 CPU 內(nèi)核數(shù)量來創(chuàng)建線程,假設(shè)是 8。我們把這些功能點(diǎn)送到這 8 個(gè)線程中。當(dāng)我們處理這些功能點(diǎn)時(shí),可能會(huì)調(diào)用一些函數(shù)作為功能的一部分。寫那個(gè)函數(shù)的人也想要使用并發(fā),因此當(dāng)你調(diào)用這個(gè)函數(shù)的時(shí)候,這個(gè)函數(shù)也會(huì)創(chuàng)建 8 個(gè)線程?,F(xiàn)在,你有了 8 × 8 = 64 個(gè)線程,盡管你只有 8 個(gè)CPU內(nèi)核——也就是說任何時(shí)候只有12%的線程實(shí)際在運(yùn)行而另外88%的線程什么事情都沒做。使用 GCD 你就不會(huì)遇到這種問題,當(dāng)系統(tǒng)關(guān)閉 CPU 內(nèi)核以省電時(shí),GCD 甚至能夠相應(yīng)地調(diào)整線程數(shù)量。
GCD 通過創(chuàng)建所謂的線程池來大致匹配 CPU 內(nèi)核數(shù)量。要記住,線程的創(chuàng)建并不是無代價(jià)的。每個(gè)線程都需要占用內(nèi)存和內(nèi)核資源。這里也有一個(gè)問題:如果你提交了一個(gè) block 給 GCD,但是這段代碼阻塞了這個(gè)線程,那么這個(gè)線程在這段時(shí)間內(nèi)就不能用來完成其他工作——它被阻塞了。為了確保功能點(diǎn)在隊(duì)列上一直是執(zhí)行的,GCD 不得不創(chuàng)建一個(gè)新的線程,并把它添加到線程池。
如果你的代碼阻塞了許多線程,這會(huì)帶來很大的問題。首先,線程消耗資源,此外,創(chuàng)建線程會(huì)變得代價(jià)高昂。創(chuàng)建過程需要一些時(shí)間。并且在這段時(shí)間中,GCD 無法以全速來完成功能點(diǎn)。有不少能夠?qū)е戮€程阻塞的情況,但是最常見的情況與 I/O 有關(guān),也就是從文件或者網(wǎng)絡(luò)中讀寫數(shù)據(jù)。正是因?yàn)檫@些原因,你不應(yīng)該在GCD隊(duì)列中以阻塞的方式來做這些操作??匆幌孪旅娴?a href="#input_output">輸入輸出段落去了解一些關(guān)于如何以 GCD 運(yùn)行良好的方式來做 I/O 操作的信息。
你能夠?yàn)槟銊?chuàng)建的任何一個(gè)隊(duì)列設(shè)置一個(gè)目標(biāo)隊(duì)列。這會(huì)是很強(qiáng)大的,并且有助于調(diào)試。
為一個(gè)類創(chuàng)建它自己的隊(duì)列而不是使用全局的隊(duì)列被普遍認(rèn)為是一種好的風(fēng)格。這種方式下,你可以設(shè)置隊(duì)列的名字,這讓調(diào)試變得輕松許多—— Xcode 可以讓你在 Debug Navigator 中看到所有的隊(duì)列名字,如果你直接使用 lldb。(lldb) thread list 命令將會(huì)在控制臺(tái)打印出所有隊(duì)列的名字。一旦你使用大量的異步內(nèi)容,這會(huì)是非常有用的幫助。
使用私有隊(duì)列同樣強(qiáng)調(diào)封裝性。這時(shí)你自己的隊(duì)列,你要自己決定如何使用它。
默認(rèn)情況下,一個(gè)新創(chuàng)建的隊(duì)列轉(zhuǎn)發(fā)到默認(rèn)優(yōu)先級(jí)的全局隊(duì)列中。我們就將會(huì)討論一些有關(guān)優(yōu)先級(jí)的東西。
你可以改變你隊(duì)列轉(zhuǎn)發(fā)到的隊(duì)列——你可以設(shè)置自己隊(duì)列的目標(biāo)隊(duì)列。以這種方式,你可以將不同隊(duì)列鏈接在一起。你的 Foo 類有一個(gè)隊(duì)列,該隊(duì)列轉(zhuǎn)發(fā)到 Bar 類的隊(duì)列,Bar 類的隊(duì)列又轉(zhuǎn)發(fā)到全局隊(duì)列。
當(dāng)你為了隔離目的而使用一個(gè)隊(duì)列時(shí),這會(huì)非常有用。Foo 有一個(gè)隔離隊(duì)列,并且轉(zhuǎn)發(fā)到 Bar 的隔離隊(duì)列,與 Bar 的隔離隊(duì)列所保護(hù)的有關(guān)的資源,會(huì)自動(dòng)成為線程安全的。
如果你希望多個(gè) block 同時(shí)運(yùn)行,那要確保你自己的隊(duì)列是并發(fā)的。同時(shí)需要注意,如果一個(gè)隊(duì)列的目標(biāo)隊(duì)列是串行的(也就是非并發(fā)),那么實(shí)際上這個(gè)隊(duì)列也會(huì)轉(zhuǎn)換為一個(gè)串行隊(duì)列。
你可以通過設(shè)置目標(biāo)隊(duì)列為一個(gè)全局隊(duì)列來改變自己隊(duì)列的優(yōu)先級(jí),但是你應(yīng)該克制這么做的沖動(dòng)。
在大多數(shù)情況下,改變優(yōu)先級(jí)不會(huì)使事情照你預(yù)想的方向運(yùn)行。一些看起簡單的事情實(shí)際上是一個(gè)非常復(fù)雜的問題。你很容易會(huì)碰到一個(gè)叫做優(yōu)先級(jí)反轉(zhuǎn)的情況。我們的文章《并發(fā)編程:API 及挑戰(zhàn)》有更多關(guān)于這個(gè)問題的信息,這個(gè)問題幾乎導(dǎo)致了NASA的探路者火星漫游器變成磚頭。
此外,使用 DISPATCH_QUEUE_PRIORITY_BACKGROUND 隊(duì)列時(shí),你需要格外小心。除非你理解了 throttled I/O 和 background status as per setpriority(2) 的意義,否則不要使用它。不然,系統(tǒng)可能會(huì)以難以忍受的方式終止你的 app 的運(yùn)行。打算以不干擾系統(tǒng)其他正在做 I/O 操作的方式去做 I/O 操作時(shí),一旦和優(yōu)先級(jí)反轉(zhuǎn)情況結(jié)合起來,這會(huì)變成一種危險(xiǎn)的情況。
隔離隊(duì)列是 GCD 隊(duì)列使用中非常普遍的一種模式。這里有兩個(gè)變種。
多線程編程中,最常見的情形是你有一個(gè)資源,每次只有一個(gè)線程被允許訪問這個(gè)資源。
我們?cè)?a rel="nofollow" >有關(guān)多線程技術(shù)的文章中討論了資源在并發(fā)編程中意味著什么,它通常就是一塊內(nèi)存或者一個(gè)對(duì)象,每次只有一個(gè)線程可以訪問它。
舉例來說,我們需要以多線程(或者多個(gè)隊(duì)列)方式訪問 NSMutableDictionary 。我們可能會(huì)照下面的代碼來做:
- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
key = [key copy];
dispatch_async(self.isolationQueue, ^(){
if (count == 0) {
[self.counts removeObjectForKey:key];
} else {
self.counts[key] = @(count);
}
});
}
- (NSUInteger)countForKey:(NSString *)key;
{
__block NSUInteger count;
dispatch_sync(self.isolationQueue, ^(){
NSNumber *n = self.counts[key];
count = [n unsignedIntegerValue];
});
return count;
}
通過以上代碼,只有一個(gè)線程可以訪問 NSMutableDictionary 的實(shí)例。
注意以下四點(diǎn):
async 方式來保存值,這很重要。我們不想也不必阻塞當(dāng)前線程只是為了等待寫操作完成。當(dāng)讀操作時(shí),我們使用 sync 因?yàn)槲覀冃枰祷刂怠?/li>
-setCount:forKey: 需要一個(gè) NSString 參數(shù),用來傳遞給 dispatch_async。函數(shù)調(diào)用者可以自由傳遞一個(gè) NSMutableString 值并且能夠在函數(shù)返回后修改它。因此我們必須對(duì)傳入的字符串使用 copy 操作以確保函數(shù)能夠正確地工作。如果傳入的字符串不是可變的(也就是正常的 NSString 類型),調(diào)用copy基本上是個(gè)空操作。isolationQueue 創(chuàng)建時(shí),參數(shù) dispatch_queue_attr_t 的值必須是DISPATCH_QUEUE_SERIAL(或者0)。我們能夠改善上面的那個(gè)例子。GCD 有可以讓多線程運(yùn)行的并發(fā)隊(duì)列。我們能夠安全地使用多線程來從 NSMutableDictionary 中讀取只要我們不同時(shí)修改它。當(dāng)我們需要改變這個(gè)字典時(shí),我們使用 barrier 來分發(fā)這個(gè) block。這樣的一個(gè) block 的運(yùn)行時(shí)機(jī)是,在它之前所有計(jì)劃好的 block 完成之后,并且在所有它后面的 block 運(yùn)行之前。
以如下方式創(chuàng)建隊(duì)列:
self.isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT);
并且用以下代碼來改變setter函數(shù):
- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
key = [key copy];
dispatch_barrier_async(self.isolationQueue, ^(){
if (count == 0) {
[self.counts removeObjectForKey:key];
} else {
self.counts[key] = @(count);
}
});
}
當(dāng)使用并發(fā)隊(duì)列時(shí),要確保所有的 barrier 調(diào)用都是 async 的。如果你使用 dispatch_barrier_sync ,那么你很可能會(huì)使你自己(更確切的說是,你的代碼)產(chǎn)生死鎖。寫操作需要 barrier,并且可以是 async 的。
首先,這里有一個(gè)警告:上面這個(gè)例子中我們保護(hù)的資源是一個(gè) NSMutableDictionary,出于這樣的目的,這段代碼運(yùn)行地相當(dāng)不錯(cuò)。但是在真實(shí)的代碼中,把隔離放到正確的復(fù)雜度層級(jí)下是很重要的。
如果你對(duì) NSMutableDictionary 的訪問操作變得非常頻繁,你會(huì)碰到一個(gè)已知的叫做鎖競爭的問題。鎖競爭并不是只是在 GCD 和隊(duì)列下才變得特殊,任何使用了鎖機(jī)制的程序都會(huì)碰到同樣的問題——只不過不同的鎖機(jī)制會(huì)以不同的方式碰到。
所有對(duì) dispatch_async,dispatch_sync 等等的調(diào)用都需要完成某種形式的鎖——以確保僅有一個(gè)線程或者特定的線程運(yùn)行指定的代碼。GCD 某些程序上可以使用時(shí)序(譯注:原詞為 scheduling)來避免使用鎖,但在最后,問題只是稍有變化。根本問題仍然存在:如果你有大量的線程在相同時(shí)間去訪問同一個(gè)鎖或者隊(duì)列,你就會(huì)看到性能的變化。性能會(huì)嚴(yán)重下降。
你應(yīng)該從直接復(fù)雜層次中隔離開。當(dāng)你發(fā)現(xiàn)了性能下降,這明顯表明代碼中存在設(shè)計(jì)問題。這里有兩個(gè)開銷需要你來平衡。第一個(gè)是獨(dú)占臨界區(qū)資源太久的開銷,以至于別的線程都因?yàn)檫M(jìn)入臨界區(qū)的操作而阻塞。第二個(gè)是太頻繁出入臨界區(qū)的開銷。在 GCD 的世界里,第一種開銷的情況就是一個(gè) block 在隔離隊(duì)列中運(yùn)行,它可能潛在的阻塞了其他將要在這個(gè)隔離隊(duì)列中運(yùn)行的代碼。第二種開銷對(duì)應(yīng)的就是調(diào)用 dispatch_async 和 dispatch_sync 。無論再怎么優(yōu)化,這兩個(gè)操作都不是無代價(jià)的。
令人憂傷的,不存在通用的標(biāo)準(zhǔn)來指導(dǎo)如何正確的平衡,你需要自己評(píng)測(cè)和調(diào)整。啟動(dòng) Instruments 觀察你的 app 忙于什么操作。
如果你看上面例子中的代碼,我們的臨界區(qū)代碼僅僅做了很簡單的事情。這可能是也可能不是好的方式,依賴于它怎么被使用。
在你自己的代碼中,要考慮自己是否在更高的層次保護(hù)了隔離隊(duì)列。舉個(gè)例子,類 Foo 有一個(gè)隔離隊(duì)列并且它本身保護(hù)著對(duì) NSMutableDictionary 的訪問,代替的,可以有一個(gè)用到了 Foo 類的 Bar 類有一個(gè)隔離隊(duì)列保護(hù)所有對(duì)類 Foo 的使用。換句話說,你可以把類 Foo 變?yōu)榉蔷€程安全的(沒有隔離隊(duì)列),并在 Bar 中,使用一個(gè)隔離隊(duì)列來確保任何時(shí)刻只能有一個(gè)線程使用 Foo 。
我們?cè)谶@稍稍轉(zhuǎn)變以下話題。正如你在上面看到的,你可以同步和異步地分發(fā)一個(gè) block,一個(gè)工作單元。我們?cè)?a rel="nofollow" >《并發(fā)編程:API 及挑戰(zhàn)》)中討論的一個(gè)非常普遍的問題就是死鎖。在 GCD 中,以同步分發(fā)的方式非常容易出現(xiàn)這種情況。見下面的代碼:
dispatch_queue_t queueA; // assume we have this
dispatch_sync(queueA, ^(){
dispatch_sync(queueA, ^(){
foo();
});
});
一旦我們進(jìn)入到第二個(gè) dispatch_sync 就會(huì)發(fā)生死鎖。我們不能分發(fā)到queueA,因?yàn)橛腥耍ó?dāng)前線程)正在隊(duì)列中并且永遠(yuǎn)不會(huì)離開。但是有更隱晦的產(chǎn)生死鎖方式:
dispatch_queue_t queueA; // assume we have this
dispatch_queue_t queueB; // assume we have this
dispatch_sync(queueA, ^(){
foo();
});
void foo(void)
{
dispatch_sync(queueB, ^(){
bar();
});
}
void bar(void)
{
dispatch_sync(queueA, ^(){
baz();
});
}
單獨(dú)的每次調(diào)用 dispatch_sync() 看起來都沒有問題,但是一旦組合起來,就會(huì)發(fā)生死鎖。
這是使用同步分發(fā)存在的固有問題,如果我們使用異步分發(fā),比如:
dispatch_queue_t queueA; // assume we have this
dispatch_async(queueA, ^(){
dispatch_async(queueA, ^(){
foo();
});
});
一切運(yùn)行正常。異步調(diào)用不會(huì)產(chǎn)生死鎖。因此值得我們?cè)谌魏慰赡艿臅r(shí)候都使用異步分發(fā)。我們使用一個(gè)異步調(diào)用結(jié)果 block 的函數(shù),來代替編寫一個(gè)返回值(必須要用同步)的方法或者函數(shù)。這種方式,我們會(huì)有更少發(fā)生死鎖的可能性。
異步調(diào)用的副作用就是它們很難調(diào)試。當(dāng)我們?cè)谡{(diào)試器里中止代碼運(yùn)行,回溯并查看已經(jīng)變得沒有意義了。
要牢記這些。死鎖通常是最難處理的問題。
如果你正在給設(shè)計(jì)一個(gè)給別人(或者是給自己)使用的 API,你需要記住幾種好的實(shí)踐。
正如我們剛剛提到的,你需要傾向于異步 API。當(dāng)你創(chuàng)建一個(gè) API,它會(huì)在你的控制之外以各種方式調(diào)用,如果你的代碼能產(chǎn)生死鎖,那么死鎖就會(huì)發(fā)生。
如果你需要寫的函數(shù)或者方法,那么讓它們調(diào)用 dispatch_async() 。不要讓你的函數(shù)調(diào)用者來這么做,這個(gè)調(diào)用應(yīng)該在你的方法或者函數(shù)中來做。
如果你的方法或函數(shù)有一個(gè)返回值,異步地將其傳遞給一個(gè)回調(diào)處理程序。這個(gè) API 應(yīng)該是這樣的,你的方法或函數(shù)同時(shí)持有一個(gè)結(jié)果 block 和一個(gè)將結(jié)果傳遞過去的隊(duì)列。你函數(shù)的調(diào)用者不需要自己來做分發(fā)。這么做的原因很簡單:幾乎所有時(shí)間,函數(shù)調(diào)用都應(yīng)該在一個(gè)適當(dāng)?shù)年?duì)列中,而且以這種方式編寫的代碼是很容易閱讀的??傊愕暮瘮?shù)將會(huì)(必須)調(diào)用 dispatch_async() 去運(yùn)行回調(diào)處理程序,所以它同時(shí)也可能在需要調(diào)用的隊(duì)列上做這些工作。
如果你寫一個(gè)類,讓你類的使用者設(shè)置一個(gè)回調(diào)處理隊(duì)列或許會(huì)是一個(gè)好的選擇。你的代碼可能像這樣:
- (void)processImage:(UIImage *)image completionHandler:(void(^)(BOOL success))handler;
{
dispatch_async(self.isolationQueue, ^(void){
// do actual processing here
dispatch_async(self.resultQueue, ^(void){
handler(YES);
});
});
}
如果你以這種方式來寫你的類,讓類之間協(xié)同工作就會(huì)變得容易。如果類 A 使用了類 B,它會(huì)把自己的隔離隊(duì)列設(shè)置為 B 的回調(diào)隊(duì)列。
如果你正在倒弄一些數(shù)字,并且手頭上的問題可以拆分出同樣性質(zhì)的部分,那么 dispatch_apply 會(huì)很有用。
如果你的代碼看起來是這樣的:
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
// Do something with x and y here
}
}
小小的改動(dòng)或許就可以讓它運(yùn)行的更快:
dispatch_apply(height, dispatch_get_global_queue(0, 0), ^(size_t y) {
for (size_t x = 0; x < width; x += 2) {
// Do something with x and y here
}
});
代碼運(yùn)行良好的程度取決于你在循環(huán)內(nèi)部做的操作。
block 中運(yùn)行的工作必須是非常重要的,否則這個(gè)頭部信息就顯得過于繁重了。除非代碼受到計(jì)算帶寬的約束,每個(gè)工作單元為了很好適應(yīng)緩存大小而讀寫的內(nèi)存都是臨界的。這會(huì)對(duì)性能會(huì)帶來顯著的影響。受到臨界區(qū)約束的代碼可能不會(huì)很好地運(yùn)行。詳細(xì)討論這些問題已經(jīng)超出了這篇文章的范圍。使用 dispatch_apply 可能會(huì)對(duì)性能提升有所幫助,但是性能優(yōu)化本身就是個(gè)很復(fù)雜的主題。維基百科上有一篇關(guān)于 Memory-bound function 的文章。內(nèi)存訪問速度在 L2,L3 和主存上變化很顯著。當(dāng)你的數(shù)據(jù)訪問模式與緩存大小不匹配時(shí),10倍性能下降的情況并不少見。
很多時(shí)候,你發(fā)現(xiàn)需要將異步的 block 組合起來去完成一個(gè)給定的任務(wù)。這些任務(wù)中甚至有些是并行的?,F(xiàn)在,如果你想要在這些任務(wù)都執(zhí)行完成后運(yùn)行一些代碼,"groups" 可以完成這項(xiàng)任務(wù)??催@里的例子:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^(){
// Do something that takes a while
[self doSomeFoo];
dispatch_group_async(group, dispatch_get_main_queue(), ^(){
self.foo = 42;
});
});
dispatch_group_async(group, queue, ^(){
// Do something else that takes a while
[self doSomeBar];
dispatch_group_async(group, dispatch_get_main_queue(), ^(){
self.bar = 1;
});
});
// This block will run once everything above is done:
dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
NSLog(@"foo: %d", self.foo);
NSLog(@"bar: %d", self.bar);
});
需要注意的重要事情是,所有的這些都是非阻塞的。我們從未讓當(dāng)前的線程一直等待直到別的任務(wù)做完。恰恰相反,我們只是簡單的將多個(gè) block 放入隊(duì)列。由于代碼不會(huì)阻塞,所以就不會(huì)產(chǎn)生死鎖。
同時(shí)需要注意的是,在這個(gè)小并且簡單的例子中,我們是怎么在不同的隊(duì)列間進(jìn)切換的。
一旦你將 groups 作為你的工具箱中的一部分,你可能會(huì)懷疑為什么大多數(shù)的異步API不把 dispatch_group_t 作為一個(gè)可選參數(shù)。這沒有什么無法接受的理由,僅僅是因?yàn)樽约禾砑舆@個(gè)功能太簡單了,但是你還是要小心以確保自己使用 groups 的代碼是成對(duì)出現(xiàn)的。
舉例來說,我們可以給 Core Data 的 -performBlock: API 函數(shù)添加上 groups,就像這樣:
- (void)withGroup:(dispatch_group_t)group performBlock:(dispatch_block_t)block
{
if (group == NULL) {
[self performBlock:block];
} else {
dispatch_group_enter(group);
[self performBlock:^(){
block();
dispatch_group_leave(group);
}];
}
}
當(dāng) Core Data 上的一系列操作(很可能和其他的代碼組合起來)完成以后,我們可以使用 dispatch_group_notify 來運(yùn)行一個(gè) block 。
很明顯,我們可以給 NSURLConnection 做同樣的事情:
+ (void)withGroup:(dispatch_group_t)group
sendAsynchronousRequest:(NSURLRequest *)request
queue:(NSOperationQueue *)queue
completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
{
if (group == NULL) {
[self sendAsynchronousRequest:request
queue:queue
completionHandler:handler];
} else {
dispatch_group_enter(group);
[self sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
handler(response, data, error);
dispatch_group_leave(group);
}];
}
}
為了能正常工作,你需要確保:
dispatch_group_enter() 必須要在 dispatch_group_leave()之前運(yùn)行。dispatch_group_enter() 和 dispatch_group_leave() 一直是成對(duì)出現(xiàn)的(就算有錯(cuò)誤產(chǎn)生時(shí))。GCD 有一個(gè)較少人知道的特性:事件源 dispatch_source_t。
跟 GCD 一樣,它也是很底層的東西。當(dāng)你需要用到它時(shí),它會(huì)變得極其有用。它的一些使用是秘傳招數(shù),我們將會(huì)接觸到一部分的使用。但是大部分事件源在 iOS 平臺(tái)不是很有用,因?yàn)樵?iOS 平臺(tái)有諸多限制,你無法啟動(dòng)進(jìn)程(因此就沒有必要監(jiān)視進(jìn)程),也不能在你的 app bundle 之外寫數(shù)據(jù)(因此也就沒有必要去監(jiān)視文件)等等。
GCD 事件源是以極其資源高效的方式實(shí)現(xiàn)的。
如果一些進(jìn)程正在運(yùn)行而你想知道他們什么時(shí)候存在,GCD 能夠做到這些。你也可以使用 GCD 來檢測(cè)進(jìn)程什么時(shí)候分叉,也就是產(chǎn)生子進(jìn)程或者傳送給了進(jìn)程的一個(gè)信號(hào)(比如 SIGTERM)。
NSRunningApplication *mail = [NSRunningApplication
runningApplicationsWithBundleIdentifier:@"com.apple.mail"];
if (mail == nil) {
return;
}
pid_t const pid = mail.processIdentifier;
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid,
DISPATCH_PROC_EXIT, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(self.source, ^(){
NSLog(@"Mail quit.");
});
dispatch_resume(self.source);
當(dāng) Mail.app 退出的時(shí)候,這個(gè)程序會(huì)打印出 Mail quit.。
注意:在所有的事件源被傳遞到你的事件處理器之前,必須調(diào)用 dispatch_resume()。
這種可能性是無窮的。你能直接監(jiān)視一個(gè)文件的改變,并且當(dāng)改變發(fā)生時(shí)事件源的事件處理將會(huì)被調(diào)用。
你也可以使用它來監(jiān)視文件夾,比如創(chuàng)建一個(gè) watch folder:
NSURL *directoryURL; // assume this is set to a directory
int const fd = open([[directoryURL path] fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
char buffer[80];
strerror_r(errno, buffer, sizeof(buffer));
NSLog(@"Unable to open \"%@\": %s (%d)", [directoryURL path], buffer, errno);
return;
}
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd,
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
unsigned long const data = dispatch_source_get_data(source);
if (data & DISPATCH_VNODE_WRITE) {
NSLog(@"The directory changed.");
}
if (data & DISPATCH_VNODE_DELETE) {
NSLog(@"The directory has been deleted.");
}
});
dispatch_source_set_cancel_handler(source, ^(){
close(fd);
});
self.source = source;
dispatch_resume(self.source);
你應(yīng)該總是添加 DISPATCH_VNODE_DELETE 去檢測(cè)文件或者文件夾是否已經(jīng)被刪除——然后就停止監(jiān)聽。
大多數(shù)情況下,對(duì)于定時(shí)事件你會(huì)選擇 NSTimer。定時(shí)器的GCD版本是底層的,它會(huì)給你更多控制權(quán)——但要小心使用。
需要特別重點(diǎn)指出的是,為了讓 OS 節(jié)省電量,需要為 GCD 的定時(shí)器接口指定一個(gè)低的余地值(譯注:原文leeway value)。如果你不必要的指定了一個(gè)低余地值,將會(huì)浪費(fèi)更多的電量。
這里我們?cè)O(shè)定了一個(gè)5秒的定時(shí)器,并允許有十分之一秒的余地值:
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
NSLog(@"Time flies.");
});
dispatch_time_t start
dispatch_source_set_timer(source, DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC,
100ull * NSEC_PER_MSEC);
self.source = source;
dispatch_resume(self.source);
所有的事件源都允許你添加一個(gè) cancel handler 。這對(duì)清理你為事件源創(chuàng)建的任何資源都是很有幫助的,比如關(guān)閉文件描述符。GCD 保證在 cancel handle 調(diào)用前,所有的事件處理都已經(jīng)完成調(diào)用。
參考上面的監(jiān)視文件例子中對(duì) dispatch_source_set_cancel_handler() 的使用。
寫出能夠在繁重的 I/O 處理情況下運(yùn)行良好的代碼是一件非常棘手的事情。GCD 有一些能夠幫上忙的地方。不會(huì)涉及太多的細(xì)節(jié),我們只簡單的分析下問題是什么,GCD 是怎么處理的。
習(xí)慣上,當(dāng)你從一個(gè)網(wǎng)絡(luò)套接字中讀取數(shù)據(jù)時(shí),你要么做一個(gè)阻塞的讀操作,也就是讓你個(gè)線程一直等待直到數(shù)據(jù)變得可用,或者是做反復(fù)的輪詢。這兩種方法都是很浪費(fèi)資源并且無法度量。然而,kqueue 通過當(dāng)數(shù)據(jù)變得可用時(shí)傳遞一個(gè)事件解決了輪詢的問題,GCD 也采用了同樣的方法,但是更加優(yōu)雅。當(dāng)向套接字寫數(shù)據(jù)時(shí),同樣的問題也存在,這時(shí)你要么做阻塞的寫操作,要么等待套接字直到能夠接收數(shù)據(jù)。
在處理 I/O 時(shí),還有一個(gè)問題就是數(shù)據(jù)是以數(shù)據(jù)塊的形式到達(dá)的。當(dāng)從網(wǎng)絡(luò)中讀取數(shù)據(jù)時(shí),依據(jù) MTU(最大傳輸單元),數(shù)據(jù)塊典型的大小是在1.5K字節(jié)左右。這使得數(shù)據(jù)塊內(nèi)可以是任何內(nèi)容。一旦數(shù)據(jù)到達(dá),你通常只是對(duì)跨多個(gè)數(shù)據(jù)塊的內(nèi)容感興趣。而且通常你會(huì)在一個(gè)大的緩沖區(qū)里將數(shù)據(jù)組合起來然后再進(jìn)行處理。假設(shè)(人為例子)你收到了這樣8個(gè)數(shù)據(jù)塊:
0: HTTP/1.1 200 OK\r\nDate: Mon, 23 May 2005 22:38
1: :34 GMT\r\nServer: Apache/1.3.3.7 (Unix) (Red-H
2: at/Linux)\r\nLast-Modified: Wed, 08 Jan 2003 23
3: :11:55 GMT\r\nEtag: "3f80f-1b6-3e1cb03b"\r\nCon
4: tent-Type: text/html; charset=UTF-8\r\nContent-
5: Length: 131\r\nConnection: close\r\n\r\n<html>\r
6: \n<head>\r\n <title>An Example Page</title>\r\n
7: </head>\r\n<body>\r\n Hello World, this is a ve
如果你是在尋找 HTTP 的頭部,將所有數(shù)據(jù)塊組合成一個(gè)大的緩沖區(qū)并且從中查找 \r\n\r\n 是非常簡單的。但是這樣做,你會(huì)大量地復(fù)制這些數(shù)據(jù)。大量 舊的 C 語言 API 存在的另一個(gè)問題就是,緩沖區(qū)沒有所有權(quán)的概念,所以函數(shù)不得不將數(shù)據(jù)再次拷貝到自己的緩沖區(qū)中——又一次的拷貝??截悢?shù)據(jù)操作看起來是無關(guān)緊要的,但是當(dāng)你正在做大量的 I/O 操作的時(shí)候,你會(huì)在 profiling tool(Instruments) 中看到這些拷貝操作大量出現(xiàn)。即使你僅僅每個(gè)內(nèi)存區(qū)域拷貝一次,你還是使用了兩倍的存儲(chǔ)帶寬并且占用了兩倍的內(nèi)存緩存。
最直接了當(dāng)?shù)姆椒ㄊ鞘褂脭?shù)據(jù)緩沖區(qū)。GCD 有一個(gè) dispatch_data_t 類型,在某種程度上和 Objective-C 的 NSData 類型很相似。但是它能做別的事情,而且更通用。
注意,dispatch_data_t 可以被 retained 和 releaseed ,并且 dispatch_data_t 擁有它持有的對(duì)象。
這看起來無關(guān)緊要,但是我們必須記住 GCD 只是純 C 的 API,并且不能使用Objective-C。通常的做法是創(chuàng)建一個(gè)緩沖區(qū),這個(gè)緩沖區(qū)要么是基于棧的,要么是 malloc 操作分配的內(nèi)存區(qū)域 —— 這些都沒有所有權(quán)。
dispatch_data_t 的一個(gè)相當(dāng)獨(dú)特的屬性是它可以基于零碎的內(nèi)存區(qū)域。這解決了我們剛提到的組合內(nèi)存的問題。當(dāng)你要將兩個(gè)數(shù)據(jù)對(duì)象連接起來時(shí):
dispatch_data_t a; // Assume this hold some valid data
dispatch_data_t b; // Assume this hold some valid data
dispatch_data_t c = dispatch_data_create_concat(a, b);
數(shù)據(jù)對(duì)象 c 并不會(huì)將 a 和 b 拷貝到一個(gè)單獨(dú)的,更大的內(nèi)存區(qū)域里去。相反,它只是簡單地 retain 了 a 和 b。你可以使用 dispatch_data_apply 來遍歷對(duì)象 c 持有的內(nèi)存區(qū)域:
dispatch_data_apply(c, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
fprintf(stderr, "region with offset %zu, size %zu\n", offset, size);
return true;
});
類似的,你可以使用 dispatch_data_create_subrange 來創(chuàng)建一個(gè)不做任何拷貝操作的子區(qū)域。
在 GCD 的核心里,調(diào)度 I/O(譯注:原文為 Dispatch I/O) 與所謂的通道有關(guān)。調(diào)度 I/O 通道提供了一種與從文件描述符中讀寫不同的方式。創(chuàng)建這樣一個(gè)通道最基本的方式就是調(diào)用:
dispatch_io_t dispatch_io_create(dispatch_io_type_t type, dispatch_fd_t fd,
dispatch_queue_t queue, void (^cleanup_handler)(int error));
這將返回一個(gè)持有文件描述符的創(chuàng)建好的通道。在你通過它創(chuàng)建了通道之后,你不準(zhǔn)以任何方式修改這個(gè)文件描述符。
有兩種從根本上不同類型的通道:流和隨機(jī)存取。如果你打開了硬盤上的一個(gè)文件,你可以使用它來創(chuàng)建一個(gè)隨機(jī)存取的通道(因?yàn)檫@樣的文件描述符是可尋址的)。如果你打開了一個(gè)套接字,你可以創(chuàng)建一個(gè)流通道。
如果你想要為一個(gè)文件創(chuàng)建一個(gè)通道,你最好使用需要一個(gè)路徑參數(shù)的 dispatch_io_create_with_path ,并且讓 GCD 來打開這個(gè)文件。這是有益的,因?yàn)镚CD會(huì)延遲打開這個(gè)文件以限制相同時(shí)間內(nèi)同時(shí)打開的文件數(shù)量。
類似通常的 read(2),write(2) 和 close(2) 的操作,GCD 提供了 dispatch_io_read,dispatch_io_write 和 dispatch_io_close。無論何時(shí)數(shù)據(jù)讀完或者寫完,讀寫操作調(diào)用一個(gè)回調(diào) block 來結(jié)束。這些都是以非阻塞,異步 I/O 的形式高效實(shí)現(xiàn)的。
在這你得不到所有的細(xì)節(jié),但是這里會(huì)提供一個(gè)創(chuàng)建TCP服務(wù)端的例子:
首先我們創(chuàng)建一個(gè)監(jiān)聽套接字,并且設(shè)置一個(gè)接受連接的事件源:
_isolation = dispatch_queue_create([[self description] UTF8String], 0);
_nativeSocket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in sin = {};
sin.sin_len = sizeof(sin);
sin.sin_family = AF_INET6;
sin.sin_port = htons(port);
sin.sin_addr.s_addr= INADDR_ANY;
int err = bind(result.nativeSocket, (struct sockaddr *) &sin, sizeof(sin));
NSCAssert(0 <= err, @"");
_eventSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, _nativeSocket, 0, _isolation);
dispatch_source_set_event_handler(result.eventSource, ^{
acceptConnection(_nativeSocket);
});
當(dāng)接受了連接,我們創(chuàng)建一個(gè)I/O通道:
typedef union socketAddress {
struct sockaddr sa;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
} socketAddressUnion;
socketAddressUnion rsa; // remote socket address
socklen_t len = sizeof(rsa);
int native = accept(nativeSocket, &rsa.sa, &len);
if (native == -1) {
// Error. Ignore.
return nil;
}
_remoteAddress = rsa;
_isolation = dispatch_queue_create([[self description] UTF8String], 0);
_channel = dispatch_io_create(DISPATCH_IO_STREAM, native, _isolation, ^(int error) {
NSLog(@"An error occured while listening on socket: %d", error);
});
//dispatch_io_set_high_water(_channel, 8 * 1024);
dispatch_io_set_low_water(_channel, 1);
dispatch_io_set_interval(_channel, NSEC_PER_MSEC * 10, DISPATCH_IO_STRICT_INTERVAL);
socketAddressUnion lsa; // remote socket address
socklen_t len = sizeof(rsa);
getsockname(native, &lsa.sa, &len);
_localAddress = lsa;
如果我們想要設(shè)置 SO_KEEPALIVE(如果使用了HTTP的keep-alive),我們需要在調(diào)用 dispatch_io_create 前這么做。
創(chuàng)建好 I/O 通道后,我們可以設(shè)置讀取處理程序:
dispatch_io_read(_channel, 0, SIZE_MAX, _isolation, ^(bool done, dispatch_data_t data, int error){
if (data != NULL) {
if (_data == NULL) {
_data = data;
} else {
_data = dispatch_data_create_concat(_data, data);
}
[self processData];
}
});
如果所有你想做的只是讀取或者寫入一個(gè)文件,GCD 提供了兩個(gè)方便的封裝: dispatch_read 和 dispatch_write 。你需要傳遞給 dispatch_read 一個(gè)文件路徑和一個(gè)在所有數(shù)據(jù)塊讀取后調(diào)用的 block。類似的,dispatch_write 需要一個(gè)文件路徑和一個(gè)被寫入的 dispatch_data_t 對(duì)象。
在 GCD 的一個(gè)不起眼的角落,你會(huì)發(fā)現(xiàn)一個(gè)適合優(yōu)化代碼的靈巧小工具:
uint64_t dispatch_benchmark(size_t count, void (^block)(void));
把這個(gè)聲明放到你的代碼中,你就能夠測(cè)量給定的代碼執(zhí)行的平均的納秒數(shù)。例子如下:
size_t const objectCount = 1000;
uint64_t n = dispatch_benchmark(10000, ^{
@autoreleasepool {
id obj = @42;
NSMutableArray *array = [NSMutableArray array];
for (size_t i = 0; i < objectCount; ++i) {
[array addObject:obj];
}
}
});
NSLog(@"-[NSMutableArray addObject:] : %llu ns", n);
在我的機(jī)器上輸出了:
-[NSMutableArray addObject:] : 31803 ns
也就是說添加1000個(gè)對(duì)象到 NSMutableArray 總共消耗了31803納秒,或者說平均一個(gè)對(duì)象消耗32納秒。
正如 dispatch_benchmark 的幫助頁面指出的,測(cè)量性能并非如看起來那樣不重要。尤其是當(dāng)比較并發(fā)代碼和非并發(fā)代碼時(shí),你需要注意特定硬件上運(yùn)行的特定計(jì)算帶寬和內(nèi)存帶寬。不同的機(jī)器會(huì)很不一樣。如果代碼的性能與訪問臨界區(qū)有關(guān),那么我們上面提到的鎖競爭問題就會(huì)有所影響。
不要把它放到發(fā)布代碼中,事實(shí)上,這是無意義的,它是私有API。它只是在調(diào)試和性能分析上起作用。
訪問幫助界面:
curl "http://opensource.apple.com/source/libdispatch/libdispatch-84.5/man/dispatch_benchmark.3?txt"
| /usr/bin/groffer --tty -T utf8
頭文件 libkern/OSAtomic.h 里有許多強(qiáng)大的函數(shù),專門用來底層多線程編程。盡管它是內(nèi)核頭文件的一部分,它也能夠在內(nèi)核之外來幫助編程。
這些函數(shù)都是很底層的,并且你需要知道一些額外的事情。就算你已經(jīng)這樣做了,你還可能會(huì)發(fā)現(xiàn)一兩件你不能做,或者不易做的事情。當(dāng)你正在為編寫高性能代碼或者正在實(shí)現(xiàn)無鎖的和無等待的算法工作時(shí),這些函數(shù)會(huì)吸引你。
這些函數(shù)在 atomic(3) 的幫助頁里全部有概述——運(yùn)行 man 3 atomic 命令以得到完整的文檔。你會(huì)發(fā)現(xiàn)里面討論到了內(nèi)存屏障。查看維基百科中關(guān)于內(nèi)存屏障的文章。如果你還存在疑問,那么你很可能需要它。
OSAtomicIncrement 和 OSAtomicDecrement 有一個(gè)很長的函數(shù)列表允許你以原子操作的方式去增加和減少一個(gè)整數(shù)值 —— 不必使用鎖(或者隊(duì)列)同時(shí)也是線程安全的。如果你需要讓一個(gè)全局的計(jì)數(shù)器值增加,而這個(gè)計(jì)數(shù)器為了統(tǒng)計(jì)目的而由多個(gè)線程操作,使用原子操作是很有幫助的。如果你要做的僅僅是增加一個(gè)全局計(jì)數(shù)器,那么無屏障版本的 OSAtomicIncrement 是很合適的,并且當(dāng)沒有鎖競爭時(shí),調(diào)用它們的代價(jià)很小。
類似的,OSAtomicOr ,OSAtomicAnd,OSAtomicXor 的函數(shù)能用來進(jìn)行邏輯運(yùn)算,而 OSAtomicTest 可以用來設(shè)置和清除位。
OSAtomicCompareAndSwap 能用來做無鎖的惰性初始化,如下:
void * sharedBuffer(void)
{
static void * buffer;
if (buffer == NULL) {
void * newBuffer = calloc(1, 1024);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, newBuffer, &buffer)) {
free(newBuffer);
}
}
return buffer;
}
如果沒有 buffer,我們會(huì)創(chuàng)建一個(gè),然后原子地將其寫到 buffer 中如果 buffer 為NULL。在極少的情況下,其他人在當(dāng)前線程同時(shí)設(shè)置了 buffer ,我們簡單地將其釋放掉。因?yàn)楸容^和交換方法是原子的,所以它是一個(gè)線程安全的方式去惰性初始化值。NULL的檢測(cè)和設(shè)置 buffer 都是以原子方式完成的。
明顯的,使用 dispatch_once() 我們也可以完成類似的事情。
OSAtomicEnqueue() 和 OSAtomicDequeue() 可以讓你以線程安全,無鎖的方式實(shí)現(xiàn)一個(gè)LIFO隊(duì)列(常見的就是棧)。對(duì)有潛在精確要求的代碼來說,這會(huì)是強(qiáng)大的代碼。
還有 OSAtomicFifoEnqueue() 和 OSAtomicFifoDequeue() 函數(shù)是為了操作FIFO隊(duì)列,但這些只有在頭文件中才有文檔 —— 閱讀他們的時(shí)候要小心。
最后,OSAtomic.h 頭文件定義了使用自旋鎖的函數(shù):OSSpinLock。同樣的,維基百科有深入的有關(guān)自旋鎖的信息。使用命令 man 3 spinlock 查看幫助頁的 spinlock(3) 。當(dāng)沒有鎖競爭時(shí)使用自旋鎖代價(jià)很小。
在合適的情況下,使用自旋鎖對(duì)性能優(yōu)化是很有幫助的。一如既往:先測(cè)量,然后優(yōu)化。不要做樂觀的優(yōu)化。
下面是 OSSpinLock 的一個(gè)例子:
@interface MyTableViewCell : UITableViewCell
@property (readonly, nonatomic, copy) NSDictionary *amountAttributes;
@end
@implementation MyTableViewCell
{
NSDictionary *_amountAttributes;
}
- (NSDictionary *)amountAttributes;
{
if (_amountAttributes == nil) {
static __weak NSDictionary *cachedAttributes = nil;
static OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
_amountAttributes = cachedAttributes;
if (_amountAttributes == nil) {
NSMutableDictionary *attributes = [[self subtitleAttributes] mutableCopy];
attributes[NSFontAttributeName] = [UIFont fontWithName:@"ComicSans" size:36];
attributes[NSParagraphStyleAttributeName] = [NSParagraphStyle defaultParagraphStyle];
_amountAttributes = [attributes copy];
cachedAttributes = _amountAttributes;
}
OSSpinLockUnlock(&lock);
}
return _amountAttributes;
}
就上面的例子而言,或許用不著這么麻煩,但它演示了一種理念。我們使用了ARC的 __weak 來確保一旦 MyTableViewCell 所有的實(shí)例都不存在, amountAttributes 會(huì)調(diào)用 dealloc 。因此在所有的實(shí)例中,我們可以持有字典的一個(gè)單獨(dú)實(shí)例。
這段代碼運(yùn)行良好的原因是我們不太可能訪問到方法最里面的部分。這是很深?yuàn)W的——除非你真正需要,不然不要在你的 App 中使用它。