注:本人是翻譯過來,并且加上本人的一點(diǎn)見解。
前言
想要揭示出表面之下深層次的一些可利用的方面。這些底層的 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ù)添加功能并且改善它。現(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多年了,仍然非常卓越。書中簡(jiǎn)潔的寫法,優(yōu)秀的例子和練習(xí),帶你領(lǐng)略并發(fā)編程中代碼塊的基本原理。這本書現(xiàn)在已經(jīng)絕版了,但是它的一些復(fù)印版依然廣為流傳。有一個(gè)新版書,名字叫《Principles of Concurrent and Distributed Programming》,ISBN 0-321-31283-X,好像有很多相同的地方,不過我還沒有讀過。
目錄:
1. 從前
2. 延后執(zhí)行
3. 隊(duì)列
4. 目標(biāo)隊(duì)列
5. 資源保護(hù)
6. 單一資源的多讀單寫
7. 鎖競(jìng)爭(zhēng)
8. 全都使用異步分發(fā)
9. 如何寫出好的異步 API
10. 迭代執(zhí)行
11. 組
12. 對(duì)現(xiàn)有API使用 dispatchgroupt
13. 事件源
14. 監(jiān)視進(jìn)程
15. 監(jiān)視文件
16. 定時(shí)器
17. 取消
18. 輸入輸出
19. GCD 和緩沖區(qū)
20. 讀和寫
21. 基準(zhǔn)測(cè)試
22. 原子操作
23. 計(jì)數(shù)器
24. 比較和交換
25. 原子隊(duì)列
26. 自旋鎖
1. 從前
或許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)致無(wú)法預(yù)知的行為。換句話說,不要把dispatch_once_t 作為一個(gè)對(duì)象的成員變量,或者類似的情形。
退回到遠(yuǎn)古時(shí)代(其實(shí)也就是幾年前),人們會(huì)使用 pthread_once ,因?yàn)?dispatch_once_t 更容易使用并且不易出錯(cuò),所以你永遠(yuǎn)都不會(huì)再用到 pthread_once 了。
2. 延后執(zhí)行
另一個(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)試代碼就變成了一場(chǎng)噩夢(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ā)。
3. 隊(duì)列
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è)線程。現(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)建并不是無(wú)代價(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 無(wú)法以全速來完成功能點(diǎn)。有不少能夠?qū)е戮€程阻塞的情況,但是最常見的情況與 I/O 有關(guān),也就是從文件或者網(wǎng)絡(luò)中讀寫數(shù)據(jù)。正是因?yàn)檫@些原因,你不應(yīng)該在GCD隊(duì)列中以阻塞的方式來做這些操作。看一下下面的輸入輸出段落去了解一些關(guān)于如何以 GCD 運(yùn)行良好的方式來做 I/O 操作的信息。
4. 目標(biāo)隊(duì)列
你能夠?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ì)列。
5. 資源保護(hù)
多線程編程中,最常見的情形是你有一個(gè)資源,每次只有一個(gè)線程被允許訪問這個(gè)資源。它通常就是一塊內(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):
- 不要使用上面的代碼,請(qǐng)先閱讀多讀單寫和鎖競(jìng)爭(zhēng)
- 我們使用
async方式來保存值,這很重要。我們不想也不必阻塞當(dāng)前線程只是為了等待寫操作完成。當(dāng)讀操作時(shí),我們使用sync因?yàn)槲覀冃枰祷刂怠?/li> - 從函數(shù)接口可以看出,
-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)。
6. 單一資源的多讀單寫
我們能夠改善上面的那個(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 的。
7. 鎖競(jìng)爭(zhēng)
首先,這里有一個(gè)警告:上面這個(gè)例子中我們保護(hù)的資源是一個(gè) NSMutableDictionary,出于這樣的目的,這段代碼運(yùn)行地相當(dāng)不錯(cuò)。但是在真實(shí)的代碼中,把隔離放到正確的復(fù)雜度層級(jí)下是很重要的。
如果你對(duì) NSMutableDictionary 的訪問操作變得非常頻繁,你會(huì)碰到一個(gè)已知的叫做鎖競(jìng)爭(zhēng)的問題。鎖競(jìng)爭(zhēng)并不是只是在 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 。無(wú)論再怎么優(yōu)化,這兩個(gè)操作都不是無(wú)代價(jià)的。
令人憂傷的,不存在通用的標(biāo)準(zhǔn)來指導(dǎo)如何正確的平衡,你需要自己評(píng)測(cè)和調(diào)整。啟動(dòng) Instruments 觀察你的 app 忙于什么操作。
如果你看上面例子中的代碼,我們的臨界區(qū)代碼僅僅做了很簡(jiǎn)單的事情。這可能是也可能不是好的方式,依賴于它怎么被使用。
在你自己的代碼中,要考慮自己是否在更高的層次保護(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 。
8. 全都使用異步分發(fā)
我們?cè)谶@稍稍轉(zhuǎn)變以下話題。正如你在上面看到的,你可以同步和異步地分發(fā)一個(gè) block,一個(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)變得沒有意義了。
要牢記這些。死鎖通常是最難處理的問題。
9. 如何寫出好的異步 API
如果你正在給設(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ā)。這么做的原因很簡(jiǎn)單:幾乎所有時(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ì)列。
10. 迭代執(zhí)行
如果你正在倒弄一些數(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倍性能下降的情況并不少見。
11. 組
很多時(shí)候,你發(fā)現(xiàn)需要將異步的 block 組合起來去完成一個(gè)給定的任務(wù)。這些任務(wù)中甚至有些是并行的。現(xiàn)在,如果你想要在這些任務(wù)都執(zhí)行完成后運(yùn)行一些代碼,"groups" 可以完成這項(xiàng)任務(wù)。看這里的例子:
首先定義group和queue
@property (nonatomic, strong) dispatch_queue_t queue_t_a; @property (nonatomic, strong) dispatch_group_t group_t; self.queue_t_a = dispatch_queue_create("qa", 0); self.group_t = dispatch_group_create();
然后運(yùn)行
dispatch_group_async(self.group_t, _queue_t_a, ^{ sleep(3); NSLog(@"1"); }); dispatch_group_async(self.group_t, _queue_t_a, ^{ NSLog(@"2"); }); dispatch_group_notify(self.group_t, _queue_t_a, ^{ NSLog(@"3"); }); NSLog(@"viewDidAppear");
執(zhí)行打印順序永遠(yuǎn)都是viewDidAppear、1、2、3。注意這里只用到一個(gè)queue與group。dispatch_group_notify是等待上面所有queue a執(zhí)行完之后,再執(zhí)行。可以看看官網(wǎng)說明
12. 對(duì)現(xiàn)有API使用 dispatchgroupt
一旦你將 groups 作為你的工具箱中的一部分,你可能會(huì)懷疑為什么大多數(shù)的異步API不把 dispatch_group_t 作為一個(gè)可選參數(shù)。這沒有什么無(wú)法接受的理由,僅僅是因?yàn)樽约禾砑舆@個(gè)功能太簡(jiǎn)單了,但是你還是要小心以確保自己使用 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í))。
13. 事件源
GCD 有一個(gè)較少人知道的特性:事件源 dispatch_source_t。
跟 GCD 一樣,它也是很底層的東西。當(dāng)你需要用到它時(shí),它會(huì)變得極其有用。它的一些使用是秘傳招數(shù),我們將會(huì)接觸到一部分的使用。但是大部分事件源在 iOS 平臺(tái)不是很有用,因?yàn)樵?iOS 平臺(tái)有諸多限制,你無(wú)法啟動(dòng)進(jìn)程(因此就沒有必要監(jiān)視進(jìn)程),也不能在你的 app bundle 之外寫數(shù)據(jù)(因此也就沒有必要去監(jiān)視文件)等等。
GCD 事件源是以極其資源高效的方式實(shí)現(xiàn)的。
14. 監(jiān)視進(jì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()。
15. 監(jiān)視文件
這種可能性是無(wú)窮的。你能直接監(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)聽。
16. 定時(shí)器
大多數(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);
17. 取消
所有的事件源都允許你添加一個(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() 的使用。
18. 輸入輸出
寫出能夠在繁重的 I/O 處理情況下運(yùn)行良好的代碼是一件非常棘手的事情。GCD 有一些能夠幫上忙的地方。不會(huì)涉及太多的細(xì)節(jié),我們只簡(jiǎn)單的分析下問題是什么,GCD 是怎么處理的。
習(xí)慣上,當(dāng)你從一個(gè)網(wǎng)絡(luò)套接字中讀取數(shù)據(jù)時(shí),你要么做一個(gè)阻塞的讀操作,也就是讓你個(gè)線程一直等待直到數(shù)據(jù)變得可用,或者是做反復(fù)的輪詢。這兩種方法都是很浪費(fèi)資源并且無(wú)法度量。然而,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([]最大傳輸單元](https://en.wikipedia.org/wiki/Maximumtransmissionunit)),數(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 是非常簡(jiǎn)單的。但是這樣做,你會(huì)大量地復(fù)制這些數(shù)據(jù)。大量 舊的 C 語(yǔ)言 API 存在的另一個(gè)問題就是,緩沖區(qū)沒有所有權(quán)的概念,所以函數(shù)不得不將數(shù)據(jù)再次拷貝到自己的緩沖區(qū)中——又一次的拷貝。拷貝數(shù)據(jù)操作看起來是無(wú)關(guān)緊要的,但是當(dāng)你正在做大量的 I/O 操作的時(shí)候,你會(huì)在 profiling tool(Instruments) 中看到這些拷貝操作大量出現(xiàn)。即使你僅僅每個(gè)內(nèi)存區(qū)域拷貝一次,你還是使用了兩倍的存儲(chǔ)帶寬并且占用了兩倍的內(nèi)存緩存。
19. GCD 和緩沖區(qū)
最直接了當(dāng)?shù)姆椒ㄊ鞘褂脭?shù)據(jù)緩沖區(qū)。GCD 有一個(gè) dispatch_data_t 類型,在某種程度上和 Objective-C 的 NSData 類型很相似。但是它能做別的事情,而且更通用。
注意,dispatch_data_t 可以被 retained 和 releaseed ,并且 dispatch_data_t 擁有它持有的對(duì)象。
這看起來無(wú)關(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ū)域里去。相反,它只是簡(jiǎn)單地 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ū)域。
20. 讀和寫
在 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。無(wú)論何時(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ì)象。
21. 基準(zhǔn)測(cè)試
在 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 的幫助頁(yè)面指出的,測(cè)量性能并非如看起來那樣不重要。尤其是當(dāng)比較并發(fā)代碼和非并發(fā)代碼時(shí),你需要注意特定硬件上運(yùn)行的特定計(jì)算帶寬和內(nèi)存帶寬。不同的機(jī)器會(huì)很不一樣。如果代碼的性能與訪問臨界區(qū)有關(guān),那么我們上面提到的鎖競(jìng)爭(zhēng)問題就會(huì)有所影響。
不要把它放到發(fā)布代碼中,事實(shí)上,這是無(wú)意義的,它是私有API。它只是在調(diào)試和性能分析上起作用。
訪問幫助界面:
curl "http://opensource.apple.com/source/libdispatch/libdispatch-84.5/man/dispatch_benchmark.3?txt" | /usr/bin/groffer --tty -T utf8
22. 原子操作
頭文件 libkern/OSAtomic.h 里有許多強(qiáng)大的函數(shù),專門用來底層多線程編程。盡管它是內(nèi)核頭文件的一部分,它也能夠在內(nèi)核之外來幫助編程。
這些函數(shù)都是很底層的,并且你需要知道一些額外的事情。就算你已經(jīng)這樣做了,你還可能會(huì)發(fā)現(xiàn)一兩件你不能做,或者不易做的事情。當(dāng)你正在為編寫高性能代碼或者正在實(shí)現(xiàn)無(wú)鎖的和無(wú)等待的算法工作時(shí),這些函數(shù)會(huì)吸引你。
這些函數(shù)在 atomic(3) 的幫助頁(yè)里全部有概述——運(yùn)行 man 3 atomic 命令以得到完整的文檔。你會(huì)發(fā)現(xiàn)里面討論到了內(nèi)存屏障。查看維基百科中關(guān)于內(nèi)存屏障的文章。如果你還存在疑問,那么你很可能需要它。
23. 計(jì)數(shù)器
OSAtomicIncrement 和 OSAtomicDecrement 有一個(gè)很長(zhǎng)的函數(shù)列表允許你以原子操作的方式去增加和減少一個(gè)整數(shù)值 —— 不必使用鎖(或者隊(duì)列)同時(shí)也是線程安全的。如果你需要讓一個(gè)全局的計(jì)數(shù)器值增加,而這個(gè)計(jì)數(shù)器為了統(tǒng)計(jì)目的而由多個(gè)線程操作,使用原子操作是很有幫助的。如果你要做的僅僅是增加一個(gè)全局計(jì)數(shù)器,那么無(wú)屏障版本的 OSAtomicIncrement 是很合適的,并且當(dāng)沒有鎖競(jìng)爭(zhēng)時(shí),調(diào)用它們的代價(jià)很小。
類似的,OSAtomicOr ,OSAtomicAnd,OSAtomicXor 的函數(shù)能用來進(jìn)行邏輯運(yùn)算,而 OSAtomicTest 可以用來設(shè)置和清除位。
24. 比較和交換
OSAtomicCompareAndSwap 能用來做無(wú)鎖的惰性初始化,如下:
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 ,我們簡(jiǎn)單地將其釋放掉。因?yàn)楸容^和交換方法是原子的,所以它是一個(gè)線程安全的方式去惰性初始化值。NULL的檢測(cè)和設(shè)置 buffer 都是以原子方式完成的。
明顯的,使用 dispatch_once() 我們也可以完成類似的事情。
25. 原子隊(duì)列
OSAtomicEnqueue() 和 OSAtomicDequeue() 可以讓你以線程安全,無(wú)鎖的方式實(shí)現(xiàn)一個(gè)LIFO隊(duì)列(常見的就是棧)。對(duì)有潛在精確要求的代碼來說,這會(huì)是強(qiáng)大的代碼。
還有 OSAtomicFifoEnqueue() 和 OSAtomicFifoDequeue() 函數(shù)是為了操作FIFO隊(duì)列,但這些只有在頭文件中才有文檔 —— 閱讀他們的時(shí)候要小心。
26. 自旋鎖
最后,OSAtomic.h 頭文件定義了使用自旋鎖的函數(shù):OSSpinLock。同樣的,維基百科有深入的有關(guān)自旋鎖的信息。使用命令 man 3 spinlock 查看幫助頁(yè)的 spinlock(3) 。當(dāng)沒有鎖競(jìng)爭(zhē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 中使用它。
可以關(guān)注本人的公眾號(hào),多年經(jīng)驗(yàn)的原創(chuàng)文章共享給大家。

浙公網(wǎng)安備 33010602011771號(hào)