IOS 多线程04-GCD详解 底层并发 API
注:本人是翻譯過來,并且加上本人的一點見解。
?
前言
想要揭示出表面之下深層次的一些可利用的方面。這些底層的 API 提供了大量的靈活性,隨之而來的是大量的復(fù)雜度和更多的責(zé)任。在我們的文章常見的后臺實踐中提到的高層的 API 和模式能夠讓你專注于手頭的任務(wù)并且免于大量的問題。通常來說,高層的 API 會提供更好的性能,除非你能承受起使用底層 API 帶來的糾結(jié)于調(diào)試代碼的時間和努力。
盡管如此,了解深層次下的軟件堆棧工作原理還是有很有幫助的。我們希望這篇文章能夠讓你更好的了解這個平臺,同時,讓你更加感謝這些高層的 API。
首先,我們將會分析大多數(shù)組成 Grand Central Dispatch 的部分。它已經(jīng)存在了好幾年,并且蘋果公司持續(xù)添加功能并且改善它。現(xiàn)在蘋果已經(jīng)將其開源,這意味著它對其他平臺也是可用的了。最后,我們將會看一下原子操作——另外的一種底層代碼塊的集合。
或許關(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ù)印版依然廣為流傳。有一個新版書,名字叫《Principles of Concurrent and Distributed Programming》,ISBN 0-321-31283-X,好像有很多相同的地方,不過我還沒有讀過。
?
目錄:
1. 從前
2. 延后執(zhí)行
3. 隊列
4. 目標(biāo)隊列
5. 資源保護(hù)
6. 單一資源的多讀單寫
7. 鎖競爭
8. 全都使用異步分發(fā)
9. 如何寫出好的異步 API
10. 迭代執(zhí)行
11. 組
12. 對現(xiàn)有API使用 dispatchgroupt
13. 事件源
14. 監(jiān)視進(jìn)程
15. 監(jiān)視文件
16. 定時器
17. 取消
18. 輸入輸出
19. GCD 和緩沖區(qū)
20. 讀和寫
21. 基準(zhǔn)測試
22. 原子操作
23. 計數(shù)器
24. 比較和交換
25. 原子隊列
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 只會運行一次。并且在連續(xù)的調(diào)用中,這種檢查是很高效的。你能使用它來初始化全局?jǐn)?shù)據(jù)比如單例。要注意的是,使用 dispatch_once_t 會使得測試變得非常困難(單例和測試不是很好配合)。
? 要確保 onceToken 被聲明為 static ,或者有全局作用域。任何其他的情況都會導(dǎo)致無法預(yù)知的行為。換句話說,不要把dispatch_once_t 作為一個對象的成員變量,或者類似的情形。
? 退回到遠(yuǎn)古時代(其實也就是幾年前),人們會使用 pthread_once ,因為 dispatch_once_t 更容易使用并且不易出錯,所以你永遠(yuǎn)都不會再用到 pthread_once 了。
?
2. 延后執(zhí)行
? 另一個常見的小伙伴就是 dispatch_after 了。它使工作延后執(zhí)行。它是很強大的,但是要注意:你很容易就陷入到一堆麻煩中。一般用法是這樣的:
- (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];}); }? 第一眼看上去這段代碼是極好的。但是這里存在一些缺點。我們不能(直接)取消我們已經(jīng)提交到 dispatch_after 的代碼,它將會運行。
? 另外一個需要注意的事情就是,當(dāng)人們使用 dispatch_after 去處理他們代碼中存在的時序 bug 時,會存在一些有問題的傾向。一些代碼執(zhí)行的過早而你很可能不知道為什么會這樣,所以你把這段代碼放到了 dispatch_after 中,現(xiàn)在一切運行正常了。但是幾周以后,之前的工作不起作用了。由于你并不十分清楚你自己代碼的執(zhí)行次序,調(diào)試代碼就變成了一場噩夢。所以不要像上面這樣做。大多數(shù)的情況下,你最好把代碼放到正確的位置。如果代碼放到 -viewWillAppear 太早,那么或許 -viewDidAppear 就是正確的地方。
? 通過在自己代碼中建立直接調(diào)用(類似 -viewDidAppear )而不是依賴于 dispatch_after ,你會為自己省去很多麻煩。
? 如果你需要一些事情在某個特定的時刻運行,那么 dispatch_after 或許會是個好的選擇。確保同時考慮了 NSTimer,這個API雖然有點笨重,但是它允許你取消定時器的觸發(fā)。
?
3. 隊列
? GCD 中一個基本的代碼塊就是隊列。下面我們會給出一些如何使用它的例子。當(dāng)使用隊列的時候,給它們一個明顯的標(biāo)簽會幫自己不少忙。在調(diào)試時,這個標(biāo)簽會在 Xcode (和 lldb)中顯示,這會幫助你了解你的 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; }? 隊列可以是并行也可以是串行的。默認(rèn)情況下,它們是串行的,也就是說,任何給定的時間內(nèi),只能有一個單獨的 block 運行。這就是隔離隊列(原文:isolation queues。譯注)的運行方式。隊列也可以是并行的,也就是同一時間內(nèi)允許多個 block 一起執(zhí)行。
? GCD 隊列的內(nèi)部使用的是線程。GCD 管理這些線程,并且使用 GCD 的時候,你不需要自己創(chuàng)建線程。但是重要的外在部分 GCD 會呈現(xiàn)給你,也就是用戶 API,一個很大不同的抽象層級。當(dāng)使用 GCD 來完成并發(fā)的工作時,你不必考慮線程方面的問題,取而代之的,只需考慮隊列和功能點(提交給隊列的 block)。雖然往下深究,依然都是線程,但是 GCD 的抽象層級為你慣用的編碼提供了更好的方式。
? 隊列和功能點同時解決了一個連續(xù)不斷的扇出的問題:如果我們直接使用線程,并且想要做一些并發(fā)的事情,我們很可能將我們的工作分成 100 個小的功能點,然后基于可用的 CPU 內(nèi)核數(shù)量來創(chuàng)建線程,假設(shè)是 8。我們把這些功能點送到這 8 個線程中。當(dāng)我們處理這些功能點時,可能會調(diào)用一些函數(shù)作為功能的一部分。寫那個函數(shù)的人也想要使用并發(fā),因此當(dāng)你調(diào)用這個函數(shù)的時候,這個函數(shù)也會創(chuàng)建 8 個線程?,F(xiàn)在,你有了 8 × 8 = 64 個線程,盡管你只有 8 個CPU內(nèi)核——也就是說任何時候只有12%的線程實際在運行而另外88%的線程什么事情都沒做。使用 GCD 你就不會遇到這種問題,當(dāng)系統(tǒng)關(guān)閉 CPU 內(nèi)核以省電時,GCD 甚至能夠相應(yīng)地調(diào)整線程數(shù)量。
? GCD 通過創(chuàng)建所謂的線程池來大致匹配 CPU 內(nèi)核數(shù)量。要記住,線程的創(chuàng)建并不是無代價的。每個線程都需要占用內(nèi)存和內(nèi)核資源。這里也有一個問題:如果你提交了一個 block 給 GCD,但是這段代碼阻塞了這個線程,那么這個線程在這段時間內(nèi)就不能用來完成其他工作——它被阻塞了。為了確保功能點在隊列上一直是執(zhí)行的,GCD 不得不創(chuàng)建一個新的線程,并把它添加到線程池。
? 如果你的代碼阻塞了許多線程,這會帶來很大的問題。首先,線程消耗資源,此外,創(chuàng)建線程會變得代價高昂。創(chuàng)建過程需要一些時間。并且在這段時間中,GCD 無法以全速來完成功能點。有不少能夠?qū)е戮€程阻塞的情況,但是最常見的情況與 I/O 有關(guān),也就是從文件或者網(wǎng)絡(luò)中讀寫數(shù)據(jù)。正是因為這些原因,你不應(yīng)該在GCD隊列中以阻塞的方式來做這些操作。看一下下面的輸入輸出段落去了解一些關(guān)于如何以 GCD 運行良好的方式來做 I/O 操作的信息。
?
4. 目標(biāo)隊列
? 你能夠為你創(chuàng)建的任何一個隊列設(shè)置一個目標(biāo)隊列。這會是很強大的,并且有助于調(diào)試。
? 為一個類創(chuàng)建它自己的隊列而不是使用全局的隊列被普遍認(rèn)為是一種好的風(fēng)格。這種方式下,你可以設(shè)置隊列的名字,這讓調(diào)試變得輕松許多—— Xcode 可以讓你在 Debug Navigator 中看到所有的隊列名字,如果你直接使用 lldb。(lldb) thread list 命令將會在控制臺打印出所有隊列的名字。一旦你使用大量的異步內(nèi)容,這會是非常有用的幫助。
? 使用私有隊列同樣強調(diào)封裝性。這時你自己的隊列,你要自己決定如何使用它。
? 默認(rèn)情況下,一個新創(chuàng)建的隊列轉(zhuǎn)發(fā)到默認(rèn)優(yōu)先級的全局隊列中。我們就將會討論一些有關(guān)優(yōu)先級的東西。
? 你可以改變你隊列轉(zhuǎn)發(fā)到的隊列——你可以設(shè)置自己隊列的目標(biāo)隊列。以這種方式,你可以將不同隊列鏈接在一起。你的 Foo 類有一個隊列,該隊列轉(zhuǎn)發(fā)到 Bar 類的隊列,Bar 類的隊列又轉(zhuǎn)發(fā)到全局隊列。
? 當(dāng)你為了隔離目的而使用一個隊列時,這會非常有用。Foo 有一個隔離隊列,并且轉(zhuǎn)發(fā)到 Bar 的隔離隊列,與 Bar 的隔離隊列所保護(hù)的有關(guān)的資源,會自動成為線程安全的。
? 如果你希望多個 block 同時運行,那要確保你自己的隊列是并發(fā)的。同時需要注意,如果一個隊列的目標(biāo)隊列是串行的(也就是非并發(fā)),那么實際上這個隊列也會轉(zhuǎn)換為一個串行隊列。
?
5. 資源保護(hù)
? 多線程編程中,最常見的情形是你有一個資源,每次只有一個線程被允許訪問這個資源。它通常就是一塊內(nèi)存或者一個對象,每次只有一個線程可以訪問它。
? 舉例來說,我們需要以多線程(或者多個隊列)方式訪問 NSMutableDictionary 。我們可能會照下面的代碼來做:
- (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; }? 通過以上代碼,只有一個線程可以訪問 NSMutableDictionary 的實例。
? 注意以下四點:
?
6. 單一資源的多讀單寫
? 我們能夠改善上面的那個例子。GCD 有可以讓多線程運行的并發(fā)隊列。我們能夠安全地使用多線程來從 NSMutableDictionary 中讀取只要我們不同時修改它。當(dāng)我們需要改變這個字典時,我們使用 barrier 來分發(fā)這個 block。這樣的一個 block 的運行時機是,在它之前所有計劃好的 block 完成之后,并且在所有它后面的 block 運行之前。
? 以如下方式創(chuàng)建隊列:
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ā)隊列時,要確保所有的 barrier 調(diào)用都是 async 的。如果你使用 dispatch_barrier_sync ,那么你很可能會使你自己(更確切的說是,你的代碼)產(chǎn)生死鎖。寫操作需要 barrier,并且可以是 async 的。
?
7. 鎖競爭
? 首先,這里有一個警告:上面這個例子中我們保護(hù)的資源是一個 NSMutableDictionary,出于這樣的目的,這段代碼運行地相當(dāng)不錯。但是在真實的代碼中,把隔離放到正確的復(fù)雜度層級下是很重要的。
? 如果你對 NSMutableDictionary 的訪問操作變得非常頻繁,你會碰到一個已知的叫做鎖競爭的問題。鎖競爭并不是只是在 GCD 和隊列下才變得特殊,任何使用了鎖機制的程序都會碰到同樣的問題——只不過不同的鎖機制會以不同的方式碰到。
? 所有對 dispatch_async,dispatch_sync 等等的調(diào)用都需要完成某種形式的鎖——以確保僅有一個線程或者特定的線程運行指定的代碼。GCD 某些程序上可以使用時序(譯注:原詞為 scheduling)來避免使用鎖,但在最后,問題只是稍有變化。根本問題仍然存在:如果你有大量的線程在相同時間去訪問同一個鎖或者隊列,你就會看到性能的變化。性能會嚴(yán)重下降。
? 你應(yīng)該從直接復(fù)雜層次中隔離開。當(dāng)你發(fā)現(xiàn)了性能下降,這明顯表明代碼中存在設(shè)計問題。這里有兩個開銷需要你來平衡。第一個是獨占臨界區(qū)資源太久的開銷,以至于別的線程都因為進(jìn)入臨界區(qū)的操作而阻塞。第二個是太頻繁出入臨界區(qū)的開銷。在 GCD 的世界里,第一種開銷的情況就是一個 block 在隔離隊列中運行,它可能潛在的阻塞了其他將要在這個隔離隊列中運行的代碼。第二種開銷對應(yīng)的就是調(diào)用 dispatch_async 和 dispatch_sync 。無論再怎么優(yōu)化,這兩個操作都不是無代價的。
? 令人憂傷的,不存在通用的標(biāo)準(zhǔn)來指導(dǎo)如何正確的平衡,你需要自己評測和調(diào)整。啟動 Instruments 觀察你的 app 忙于什么操作。
? 如果你看上面例子中的代碼,我們的臨界區(qū)代碼僅僅做了很簡單的事情。這可能是也可能不是好的方式,依賴于它怎么被使用。
? 在你自己的代碼中,要考慮自己是否在更高的層次保護(hù)了隔離隊列。舉個例子,類 Foo 有一個隔離隊列并且它本身保護(hù)著對NSMutableDictionary 的訪問,代替的,可以有一個用到了 Foo 類的 Bar 類有一個隔離隊列保護(hù)所有對類 Foo 的使用。換句話說,你可以把類 Foo 變?yōu)榉蔷€程安全的(沒有隔離隊列),并在 Bar 中,使用一個隔離隊列來確保任何時刻只能有一個線程使用 Foo 。
?
8. 全都使用異步分發(fā)
? 我們在這稍稍轉(zhuǎn)變以下話題。正如你在上面看到的,你可以同步和異步地分發(fā)一個 block,一個工作單元。在 GCD 中,以同步分發(fā)的方式非常容易出現(xiàn)這種情況。見下面的代碼:
dispatch_queue_t queueA; // assume we have this dispatch_sync(queueA, ^(){dispatch_sync(queueA, ^(){foo();}); });? 一旦我們進(jìn)入到第二個 dispatch_sync 就會發(fā)生死鎖。我們不能分發(fā)到queueA,因為有人(當(dāng)前線程)正在隊列中并且永遠(yuǎn)不會離開。但是有更隱晦的產(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();}); }? 單獨的每次調(diào)用 dispatch_sync() 看起來都沒有問題,但是一旦組合起來,就會發(fā)生死鎖。
? 這是使用同步分發(fā)存在的固有問題,如果我們使用異步分發(fā),比如:
dispatch_queue_t queueA; // assume we have this dispatch_async(queueA, ^(){dispatch_async(queueA, ^(){foo();}); });? 一切運行正常。異步調(diào)用不會產(chǎn)生死鎖。因此值得我們在任何可能的時候都使用異步分發(fā)。我們使用一個異步調(diào)用結(jié)果 block 的函數(shù),來代替編寫一個返回值(必須要用同步)的方法或者函數(shù)。這種方式,我們會有更少發(fā)生死鎖的可能性。
? 異步調(diào)用的副作用就是它們很難調(diào)試。當(dāng)我們在調(diào)試器里中止代碼運行,回溯并查看已經(jīng)變得沒有意義了。
? 要牢記這些。死鎖通常是最難處理的問題。
?
9. 如何寫出好的異步 API
? 如果你正在給設(shè)計一個給別人(或者是給自己)使用的 API,你需要記住幾種好的實踐。
? 正如我們剛剛提到的,你需要傾向于異步 API。當(dāng)你創(chuàng)建一個 API,它會在你的控制之外以各種方式調(diào)用,如果你的代碼能產(chǎn)生死鎖,那么死鎖就會發(fā)生。
? 如果你需要寫的函數(shù)或者方法,那么讓它們調(diào)用 dispatch_async() 。不要讓你的函數(shù)調(diào)用者來這么做,這個調(diào)用應(yīng)該在你的方法或者函數(shù)中來做。
? 如果你的方法或函數(shù)有一個返回值,異步地將其傳遞給一個回調(diào)處理程序。這個 API 應(yīng)該是這樣的,你的方法或函數(shù)同時持有一個結(jié)果 block 和一個將結(jié)果傳遞過去的隊列。你函數(shù)的調(diào)用者不需要自己來做分發(fā)。這么做的原因很簡單:幾乎所有時間,函數(shù)調(diào)用都應(yīng)該在一個適當(dāng)?shù)年犃兄?#xff0c;而且以這種方式編寫的代碼是很容易閱讀的??傊?#xff0c;你的函數(shù)將會(必須)調(diào)用 dispatch_async() 去運行回調(diào)處理程序,所以它同時也可能在需要調(diào)用的隊列上做這些工作。
? 如果你寫一個類,讓你類的使用者設(shè)置一個回調(diào)處理隊列或許會是一個好的選擇。你的代碼可能像這樣:
- (void)processImage:(UIImage *)image completionHandler:(void(^)(BOOL success))handler; {dispatch_async(self.isolationQueue, ^(void){// do actual processing heredispatch_async(self.resultQueue, ^(void){handler(YES);});}); }? 如果你以這種方式來寫你的類,讓類之間協(xié)同工作就會變得容易。如果類 A 使用了類 B,它會把自己的隔離隊列設(shè)置為 B 的回調(diào)隊列。
?
10. 迭代執(zhí)行
? 如果你正在倒弄一些數(shù)字,并且手頭上的問題可以拆分出同樣性質(zhì)的部分,那么 dispatch_apply 會很有用。
? 如果你的代碼看起來是這樣的:
for (size_t y = 0; y < height; ++y) {for (size_t x = 0; x < width; ++x) {// Do something with x and y here } }? 小小的改動或許就可以讓它運行的更快:
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 } });? 代碼運行良好的程度取決于你在循環(huán)內(nèi)部做的操作。
? block 中運行的工作必須是非常重要的,否則這個頭部信息就顯得過于繁重了。除非代碼受到計算帶寬的約束,每個工作單元為了很好適應(yīng)緩存大小而讀寫的內(nèi)存都是臨界的。這會對性能會帶來顯著的影響。受到臨界區(qū)約束的代碼可能不會很好地運行。詳細(xì)討論這些問題已經(jīng)超出了這篇文章的范圍。使用 dispatch_apply 可能會對性能提升有所幫助,但是性能優(yōu)化本身就是個很復(fù)雜的主題。維基百科上有一篇關(guān)于 Memory-bound function 的文章。內(nèi)存訪問速度在 L2,L3 和主存上變化很顯著。當(dāng)你的數(shù)據(jù)訪問模式與緩存大小不匹配時,10倍性能下降的情況并不少見。
?
11. 組
? 很多時候,你發(fā)現(xiàn)需要將異步的 block 組合起來去完成一個給定的任務(wù)。這些任務(wù)中甚至有些是并行的?,F(xiàn)在,如果你想要在這些任務(wù)都執(zhí)行完成后運行一些代碼,"groups" 可以完成這項任務(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();? 然后運行
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。注意這里只用到一個queue與group。dispatch_group_notify是等待上面所有queue a執(zhí)行完之后,再執(zhí)行??梢钥纯垂倬W(wǎng)說明
?
12. 對現(xiàn)有API使用 dispatchgroupt
? 一旦你將 groups 作為你的工具箱中的一部分,你可能會懷疑為什么大多數(shù)的異步API不把 dispatch_group_t 作為一個可選參數(shù)。這沒有什么無法接受的理由,僅僅是因為自己添加這個功能太簡單了,但是你還是要小心以確保自己使用 groups 的代碼是成對出現(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 來運行一個 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()之前運行。
- dispatch_group_enter() 和 dispatch_group_leave() 一直是成對出現(xiàn)的(就算有錯誤產(chǎn)生時)。
?
13. 事件源
? GCD 有一個較少人知道的特性:事件源 dispatch_source_t。
? 跟 GCD 一樣,它也是很底層的東西。當(dāng)你需要用到它時,它會變得極其有用。它的一些使用是秘傳招數(shù),我們將會接觸到一部分的使用。但是大部分事件源在 iOS 平臺不是很有用,因為在 iOS 平臺有諸多限制,你無法啟動進(jìn)程(因此就沒有必要監(jiān)視進(jìn)程),也不能在你的 app bundle 之外寫數(shù)據(jù)(因此也就沒有必要去監(jiān)視文件)等等。
? GCD 事件源是以極其資源高效的方式實現(xiàn)的。
?
14. 監(jiān)視進(jìn)程
? 如果一些進(jìn)程正在運行而你想知道他們什么時候存在,GCD 能夠做到這些。你也可以使用 GCD 來檢測進(jìn)程什么時候分叉,也就是產(chǎn)生子進(jìn)程或者傳送給了進(jìn)程的一個信號(比如 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 退出的時候,這個程序會打印出 Mail quit.。
注意:在所有的事件源被傳遞到你的事件處理器之前,必須調(diào)用 dispatch_resume()。
?
15. 監(jiān)視文件
? 這種可能性是無窮的。你能直接監(jiān)視一個文件的改變,并且當(dāng)改變發(fā)生時事件源的事件處理將會被調(diào)用。
? 你也可以使用它來監(jiān)視文件夾,比如創(chuàng)建一個 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 去檢測文件或者文件夾是否已經(jīng)被刪除——然后就停止監(jiān)聽。
?
16. 定時器
? 大多數(shù)情況下,對于定時事件你會選擇 NSTimer。定時器的GCD版本是底層的,它會給你更多控制權(quán)——但要小心使用。
? 需要特別重點指出的是,為了讓 OS 節(jié)省電量,需要為 GCD 的定時器接口指定一個低的余地值(譯注:原文leeway value)。如果你不必要的指定了一個低余地值,將會浪費更多的電量。
? 這里我們設(shè)定了一個5秒的定時器,并允許有十分之一秒的余地值:
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. 取消
? 所有的事件源都允許你添加一個 cancel handler 。這對清理你為事件源創(chuàng)建的任何資源都是很有幫助的,比如關(guān)閉文件描述符。GCD 保證在 cancel handle 調(diào)用前,所有的事件處理都已經(jīng)完成調(diào)用。
? 參考上面的監(jiān)視文件例子中對 dispatch_source_set_cancel_handler() 的使用。
?
18. 輸入輸出
? 寫出能夠在繁重的 I/O 處理情況下運行良好的代碼是一件非常棘手的事情。GCD 有一些能夠幫上忙的地方。不會涉及太多的細(xì)節(jié),我們只簡單的分析下問題是什么,GCD 是怎么處理的。
? 習(xí)慣上,當(dāng)你從一個網(wǎng)絡(luò)套接字中讀取數(shù)據(jù)時,你要么做一個阻塞的讀操作,也就是讓你個線程一直等待直到數(shù)據(jù)變得可用,或者是做反復(fù)的輪詢。這兩種方法都是很浪費資源并且無法度量。然而,kqueue 通過當(dāng)數(shù)據(jù)變得可用時傳遞一個事件解決了輪詢的問題,GCD 也采用了同樣的方法,但是更加優(yōu)雅。當(dāng)向套接字寫數(shù)據(jù)時,同樣的問題也存在,這時你要么做阻塞的寫操作,要么等待套接字直到能夠接收數(shù)據(jù)。
? 在處理 I/O 時,還有一個問題就是數(shù)據(jù)是以數(shù)據(jù)塊的形式到達(dá)的。當(dāng)從網(wǎng)絡(luò)中讀取數(shù)據(jù)時,依據(jù) MTU([]最大傳輸單元](https://en.wikipedia.org/wiki/Maximumtransmissionunit)),數(shù)據(jù)塊典型的大小是在1.5K字節(jié)左右。這使得數(shù)據(jù)塊內(nèi)可以是任何內(nèi)容。一旦數(shù)據(jù)到達(dá),你通常只是對跨多個數(shù)據(jù)塊的內(nèi)容感興趣。而且通常你會在一個大的緩沖區(qū)里將數(shù)據(jù)組合起來然后再進(jìn)行處理。假設(shè)(人為例子)你收到了這樣8個數(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ù)塊組合成一個大的緩沖區(qū)并且從中查找 \r\n\r\n 是非常簡單的。但是這樣做,你會大量地復(fù)制這些數(shù)據(jù)。大量 舊的 C 語言 API 存在的另一個問題就是,緩沖區(qū)沒有所有權(quán)的概念,所以函數(shù)不得不將數(shù)據(jù)再次拷貝到自己的緩沖區(qū)中——又一次的拷貝。拷貝數(shù)據(jù)操作看起來是無關(guān)緊要的,但是當(dāng)你正在做大量的 I/O 操作的時候,你會在 profiling tool(Instruments) 中看到這些拷貝操作大量出現(xiàn)。即使你僅僅每個內(nèi)存區(qū)域拷貝一次,你還是使用了兩倍的存儲帶寬并且占用了兩倍的內(nèi)存緩存。
?
19. GCD 和緩沖區(qū)
? 最直接了當(dāng)?shù)姆椒ㄊ鞘褂脭?shù)據(jù)緩沖區(qū)。GCD 有一個 dispatch_data_t 類型,在某種程度上和 Objective-C 的 NSData 類型很相似。但是它能做別的事情,而且更通用。
? 注意,dispatch_data_t 可以被 retained 和 releaseed ,并且 dispatch_data_t 擁有它持有的對象。
? 這看起來無關(guān)緊要,但是我們必須記住 GCD 只是純 C 的 API,并且不能使用Objective-C。通常的做法是創(chuàng)建一個緩沖區(qū),這個緩沖區(qū)要么是基于棧的,要么是 malloc 操作分配的內(nèi)存區(qū)域 —— 這些都沒有所有權(quán)。
? dispatch_data_t 的一個相當(dāng)獨特的屬性是它可以基于零碎的內(nèi)存區(qū)域。這解決了我們剛提到的組合內(nèi)存的問題。當(dāng)你要將兩個數(shù)據(jù)對象連接起來時:
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ù)對象 c 并不會將 a 和 b 拷貝到一個單獨的,更大的內(nèi)存區(qū)域里去。相反,它只是簡單地 retain 了 a 和 b。你可以使用dispatch_data_apply 來遍歷對象 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)建一個不做任何拷貝操作的子區(qū)域。
?
20. 讀和寫
? 在 GCD 的核心里,調(diào)度 I/O(譯注:原文為 Dispatch I/O) 與所謂的通道有關(guān)。調(diào)度 I/O 通道提供了一種與從文件描述符中讀寫不同的方式。創(chuàng)建這樣一個通道最基本的方式就是調(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));? 這將返回一個持有文件描述符的創(chuàng)建好的通道。在你通過它創(chuàng)建了通道之后,你不準(zhǔn)以任何方式修改這個文件描述符。
? 有兩種從根本上不同類型的通道:流和隨機存取。如果你打開了硬盤上的一個文件,你可以使用它來創(chuàng)建一個隨機存取的通道(因為這樣的文件描述符是可尋址的)。如果你打開了一個套接字,你可以創(chuàng)建一個流通道。
? 如果你想要為一個文件創(chuàng)建一個通道,你最好使用需要一個路徑參數(shù)的 dispatch_io_create_with_path ,并且讓 GCD 來打開這個文件。這是有益的,因為GCD會延遲打開這個文件以限制相同時間內(nèi)同時打開的文件數(shù)量。
? 類似通常的 read(2),write(2) 和 close(2) 的操作,GCD 提供了 dispatch_io_read,dispatch_io_write 和dispatch_io_close。無論何時數(shù)據(jù)讀完或者寫完,讀寫操作調(diào)用一個回調(diào) block 來結(jié)束。這些都是以非阻塞,異步 I/O 的形式高效實現(xiàn)的。
? 在這你得不到所有的細(xì)節(jié),但是這里會提供一個創(chuàng)建TCP服務(wù)端的例子:
? 首先我們創(chuàng)建一個監(jiān)聽套接字,并且設(shè)置一個接受連接的事件源:
_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)建一個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];} });? 如果所有你想做的只是讀取或者寫入一個文件,GCD 提供了兩個方便的封裝: dispatch_read 和 dispatch_write 。你需要傳遞給dispatch_read 一個文件路徑和一個在所有數(shù)據(jù)塊讀取后調(diào)用的 block。類似的,dispatch_write 需要一個文件路徑和一個被寫入的 dispatch_data_t 對象。
?
21. 基準(zhǔn)測試
? 在 GCD 的一個不起眼的角落,你會發(fā)現(xiàn)一個適合優(yōu)化代碼的靈巧小工具:
uint64_t dispatch_benchmark(size_t count, void (^block)(void));? 把這個聲明放到你的代碼中,你就能夠測量給定的代碼執(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);? 在我的機器上輸出了:
-[NSMutableArray addObject:] : 31803 ns? 也就是說添加1000個對象到 NSMutableArray 總共消耗了31803納秒,或者說平均一個對象消耗32納秒。
? 正如 dispatch_benchmark 的幫助頁面指出的,測量性能并非如看起來那樣不重要。尤其是當(dāng)比較并發(fā)代碼和非并發(fā)代碼時,你需要注意特定硬件上運行的特定計算帶寬和內(nèi)存帶寬。不同的機器會很不一樣。如果代碼的性能與訪問臨界區(qū)有關(guān),那么我們上面提到的鎖競爭問題就會有所影響。
? 不要把它放到發(fā)布代碼中,事實上,這是無意義的,它是私有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 里有許多強大的函數(shù),專門用來底層多線程編程。盡管它是內(nèi)核頭文件的一部分,它也能夠在內(nèi)核之外來幫助編程。
? 這些函數(shù)都是很底層的,并且你需要知道一些額外的事情。就算你已經(jīng)這樣做了,你還可能會發(fā)現(xiàn)一兩件你不能做,或者不易做的事情。當(dāng)你正在為編寫高性能代碼或者正在實現(xiàn)無鎖的和無等待的算法工作時,這些函數(shù)會吸引你。
? 這些函數(shù)在 atomic(3) 的幫助頁里全部有概述——運行 man 3 atomic 命令以得到完整的文檔。你會發(fā)現(xiàn)里面討論到了內(nèi)存屏障。查看維基百科中關(guān)于內(nèi)存屏障的文章。如果你還存在疑問,那么你很可能需要它。
?
23. 計數(shù)器
? OSAtomicIncrement 和 OSAtomicDecrement 有一個很長的函數(shù)列表允許你以原子操作的方式去增加和減少一個整數(shù)值 —— 不必使用鎖(或者隊列)同時也是線程安全的。如果你需要讓一個全局的計數(shù)器值增加,而這個計數(shù)器為了統(tǒng)計目的而由多個線程操作,使用原子操作是很有幫助的。如果你要做的僅僅是增加一個全局計數(shù)器,那么無屏障版本的 OSAtomicIncrement 是很合適的,并且當(dāng)沒有鎖競爭時,調(diào)用它們的代價很小。
? 類似的,OSAtomicOr ,OSAtomicAnd,OSAtomicXor 的函數(shù)能用來進(jìn)行邏輯運算,而 OSAtomicTest 可以用來設(shè)置和清除位。
?
24. 比較和交換
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,我們會創(chuàng)建一個,然后原子地將其寫到 buffer 中如果 buffer 為NULL。在極少的情況下,其他人在當(dāng)前線程同時設(shè)置了 buffer ,我們簡單地將其釋放掉。因為比較和交換方法是原子的,所以它是一個線程安全的方式去惰性初始化值。NULL的檢測和設(shè)置 buffer 都是以原子方式完成的。
? 明顯的,使用 dispatch_once() 我們也可以完成類似的事情。
?
25. 原子隊列
? OSAtomicEnqueue() 和 OSAtomicDequeue() 可以讓你以線程安全,無鎖的方式實現(xiàn)一個LIFO隊列(常見的就是棧)。對有潛在精確要求的代碼來說,這會是強大的代碼。
? 還有 OSAtomicFifoEnqueue() 和 OSAtomicFifoDequeue() 函數(shù)是為了操作FIFO隊列,但這些只有在頭文件中才有文檔 —— 閱讀他們的時候要小心。
?
26. 自旋鎖
? 最后,OSAtomic.h 頭文件定義了使用自旋鎖的函數(shù):OSSpinLock。同樣的,維基百科有深入的有關(guān)自旋鎖的信息。使用命令 man 3 spinlock 查看幫助頁的 spinlock(3) 。當(dāng)沒有鎖競爭時使用自旋鎖代價很小。
? 在合適的情況下,使用自旋鎖對性能優(yōu)化是很有幫助的。一如既往:先測量,然后優(yōu)化。不要做樂觀的優(yōu)化。
? 下面是 OSSpinLock 的一個例子:
@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 所有的實例都不存在, amountAttributes 會調(diào)用 dealloc 。因此在所有的實例中,我們可以持有字典的一個單獨實例。
? 這段代碼運行良好的原因是我們不太可能訪問到方法最里面的部分。這是很深奧的——除非你真正需要,不然不要在你的 App 中使用它。
?
可以關(guān)注本人的公眾號,多年經(jīng)驗的原創(chuàng)文章共享給大家。
posted on 2016-06-22 17:03 alun-chen 閱讀(...) 評論(...) 編輯 收藏轉(zhuǎn)載于:https://www.cnblogs.com/alunchen/p/5607821.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的IOS 多线程04-GCD详解 底层并发 API的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 孕妇梦到和老公离婚是什么意思
- 下一篇: 笨办法学C 练习45:一个简单的TCP/