這篇文章里,我們將會討論一些 iOS 和 OS X 都可以使用的底層 API。除了dispatch_once,我們一般不鼓勵使用其中的任何一種技術(shù)。
但是我們想要揭示出表面之下深層次的一些可利用的方面。這些底層的 API 提供了大量的靈活性,隨之而來的是大量的復(fù)雜度和更多的責(zé)任。在我們的文章常見的后臺實踐中提到的高層的 API 和模式能夠讓你專注于手頭的任務(wù)并且免于大量的問題。通常來說,高層的 API 會提供更好的性能,除非你能承受起使用底層 API 帶來的糾結(jié)于調(diào)試代碼的時間和努力。
盡管如此,了解深層次下的軟件堆棧工作原理還是有很有幫助的。我們希望這篇文章能夠讓你更好的了解這個平臺,同時,讓你更加感謝這些高層的 API。
首先,我們將會分析大多數(shù)組成Grand Central Dispatch的部分。它已經(jīng)存在了好幾年,并且蘋果公司持續(xù)添加功能并且改善它?,F(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,好像有很多相同的地方,不過我還沒有讀過。
從前...
或許GCD中使用最多并且被濫用功能的就是dispatch_once了。正確的用法看起來是這樣的:

上面的 block 只會運(yùn)行一次。并且在連續(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了。
延后執(zhí)行
另一個常見的小伙伴就是dispatch_after了。它使工作延后執(zhí)行。它是很強(qiáng)大的,但是要注意:你很容易就陷入到一堆麻煩中。一般用法是這樣的:

第一眼看上去這段代碼是極好的。但是這里存在一些缺點(diǎn)。我們不能(直接)取消我們已經(jīng)提交到dispatch_after的代碼,它將會運(yùn)行。
另外一個需要注意的事情就是,當(dāng)人們使用dispatch_after去處理他們代碼中存在的時序 bug 時,會存在一些有問題的傾向。一些代碼執(zhí)行的過早而你很可能不知道為什么會這樣,所以你把這段代碼放到了dispatch_after中,現(xiàn)在一切運(yùn)行正常了。但是幾周以后,之前的工作不起作用了。由于你并不十分清楚你自己代碼的執(zhí)行次序,調(diào)試代碼就變成了一場噩夢。所以不要像上面這樣做。大多數(shù)的情況下,你最好把代碼放到正確的位置。如果代碼放到-viewWillAppear太早,那么或許-viewDidAppear就是正確的地方。
通過在自己代碼中建立直接調(diào)用(類似-viewDidAppear)而不是依賴于dispatch_after,你會為自己省去很多麻煩。
如果你需要一些事情在某個特定的時刻運(yùn)行,那么dispatch_after或許會是個好的選擇。確保同時考慮了NSTimer,這個API雖然有點(diǎn)笨重,但是它允許你取消定時器的觸發(fā)。
隊列
GCD 中一個基本的代碼塊就是隊列。下面我們會給出一些如何使用它的例子。當(dāng)使用隊列的時候,給它們一個明顯的標(biāo)簽會幫自己不少忙。在調(diào)試時,這個標(biāo)簽會在 Xcode (和 lldb)中顯示,這會幫助你了解你的 app 是由什么決定的:

隊列可以是并行也可以是串行的。默認(rèn)情況下,它們是串行的,也就是說,任何給定的時間內(nèi),只能有一個單獨(dú)的 block 運(yùn)行。這就是隔離隊列(原文:isolation queues。譯注)的運(yùn)行方式。隊列也可以是并行的,也就是同一時間內(nèi)允許多個 block 一起執(zhí)行。
GCD 隊列的內(nèi)部使用的是線程。GCD 管理這些線程,并且使用 GCD 的時候,你不需要自己創(chuàng)建線程。但是重要的外在部分 GCD 會呈現(xiàn)給你,也就是用戶 API,一個很大不同的抽象層級。當(dāng)使用 GCD 來完成并發(fā)的工作時,你不必考慮線程方面的問題,取而代之的,只需考慮隊列和功能點(diǎn)(提交給隊列的 block)。雖然往下深究,依然都是線程,但是 GCD 的抽象層級為你慣用的編碼提供了更好的方式。
隊列和功能點(diǎn)同時解決了一個連續(xù)不斷的扇出的問題:如果我們直接使用線程,并且想要做一些并發(fā)的事情,我們很可能將我們的工作分成 100 個小的功能點(diǎn),然后基于可用的 CPU 內(nèi)核數(shù)量來創(chuàng)建線程,假設(shè)是 8。我們把這些功能點(diǎn)送到這 8 個線程中。當(dāng)我們處理這些功能點(diǎn)時,可能會調(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%的線程實際在運(yùn)行而另外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)就不能用來完成其他工作——它被阻塞了。為了確保功能點(diǎn)在隊列上一直是執(zhí)行的,GCD 不得不創(chuàng)建一個新的線程,并把它添加到線程池。
如果你的代碼阻塞了許多線程,這會帶來很大的問題。首先,線程消耗資源,此外,創(chuàng)建線程會變得代價高昂。創(chuàng)建過程需要一些時間。并且在這段時間中,GCD 無法以全速來完成功能點(diǎn)。有不少能夠?qū)е戮€程阻塞的情況,但是最常見的情況與 I/O 有關(guān),也就是從文件或者網(wǎng)絡(luò)中讀寫數(shù)據(jù)。正是因為這些原因,你不應(yīng)該在GCD隊列中以阻塞的方式來做這些操作??匆幌孪旅娴?a target="_blank" rel="nofollow">輸入輸出段落去了解一些關(guān)于如何以 GCD 運(yùn)行良好的方式來做 I/O 操作的信息。
目標(biāo)隊列
你能夠為你創(chuàng)建的任何一個隊列設(shè)置一個目標(biāo)隊列。這會是很強(qiáng)大的,并且有助于調(diào)試。
為一個類創(chuàng)建它自己的隊列而不是使用全局的隊列被普遍認(rèn)為是一種好的風(fēng)格。這種方式下,你可以設(shè)置隊列的名字,這讓調(diào)試變得輕松許多—— Xcode 可以讓你在 Debug Navigator 中看到所有的隊列名字,如果你直接使用lldb。(lldb) thread list命令將會在控制臺打印出所有隊列的名字。一旦你使用大量的異步內(nèi)容,這會是非常有用的幫助。
使用私有隊列同樣強(qiáng)調(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 同時運(yùn)行,那要確保你自己的隊列是并發(fā)的。同時需要注意,如果一個隊列的目標(biāo)隊列是串行的(也就是非并發(fā)),那么實際上這個隊列也會轉(zhuǎn)換為一個串行隊列。
優(yōu)先級
你可以通過設(shè)置目標(biāo)隊列為一個全局隊列來改變自己隊列的優(yōu)先級,但是你應(yīng)該克制這么做的沖動。
在大多數(shù)情況下,改變優(yōu)先級不會使事情照你預(yù)想的方向運(yùn)行。一些看起簡單的事情實際上是一個非常復(fù)雜的問題。你很容易會碰到一個叫做優(yōu)先級反轉(zhuǎn)的情況。我們的文章《并發(fā)編程:API 及挑戰(zhàn)》有更多關(guān)于這個問題的信息,這個問題幾乎導(dǎo)致了NASA的探路者火星漫游器變成磚頭。
此外,使用DISPATCH_QUEUE_PRIORITY_BACKGROUND隊列時,你需要格外小心。除非你理解了throttled I/O和background status as per setpriority(2)的意義,否則不要使用它。不然,系統(tǒng)可能會以難以忍受的方式終止你的 app 的運(yùn)行。打算以不干擾系統(tǒng)其他正在做 I/O 操作的方式去做 I/O 操作時,一旦和優(yōu)先級反轉(zhuǎn)情況結(jié)合起來,這會變成一種危險的情況。
隔離
隔離隊列是 GCD 隊列使用中非常普遍的一種模式。這里有兩個變種。
資源保護(hù)
多線程編程中,最常見的情形是你有一個資源,每次只有一個線程被允許訪問這個資源。
我們在有關(guān)多線程技術(shù)的文章中討論了資源在并發(fā)編程中意味著什么,它通常就是一塊內(nèi)存或者一個對象,每次只有一個線程可以訪問它。
舉例來說,我們需要以多線程(或者多個隊列)方式訪問NSMutableDictionary。我們可能會照下面的代碼來做:

通過以上代碼,只有一個線程可以訪問NSMutableDictionary的實例。
注意以下四點(diǎn):
我們使用async方式來保存值,這很重要。我們不想也不必阻塞當(dāng)前線程只是為了等待寫操作完成。當(dāng)讀操作時,我們使用sync因為我們需要返回值。
從函數(shù)接口可以看出,-setCount:forKey:需要一個NSString參數(shù),用來傳遞給dispatch_async。函數(shù)調(diào)用者可以自由傳遞一個NSMutableString值并且能夠在函數(shù)返回后修改它。因此我們必須對傳入的字符串使用copy操作以確保函數(shù)能夠正確地工作。如果傳入的字符串不是可變的(也就是正常的NSString類型),調(diào)用copy基本上是個空操作。
isolationQueue創(chuàng)建時,參數(shù)dispatch_queue_attr_t的值必須是DISPATCH_QUEUE_SERIAL(或者0)。
單一資源的多讀單寫
我們能夠改善上面的那個例子。GCD 有可以讓多線程運(yùn)行的并發(fā)隊列。我們能夠安全地使用多線程來從NSMutableDictionary中讀取只要我們不同時修改它。當(dāng)我們需要改變這個字典時,我們使用barrier來分發(fā)這個 block。這樣的一個 block 的運(yùn)行時機(jī)是,在它之前所有計劃好的 block 完成之后,并且在所有它后面的 block 運(yùn)行之前。
以如下方式創(chuàng)建隊列:

并且用以下代碼來改變setter函數(shù):

當(dāng)使用并發(fā)隊列時,要確保所有的barrier調(diào)用都是async的。如果你使用dispatch_barrier_sync,那么你很可能會使你自己(更確切的說是,你的代碼)產(chǎn)生死鎖。寫操作需要barrier,并且可以是 async 的。
鎖競爭
首先,這里有一個警告:上面這個例子中我們保護(hù)的資源是一個NSMutableDictionary,出于這樣的目的,這段代碼運(yùn)行地相當(dāng)不錯。但是在真實的代碼中,把隔離放到正確的復(fù)雜度層級下是很重要的。
如果你對NSMutableDictionary的訪問操作變得非常頻繁,你會碰到一個已知的叫做鎖競爭的問題。鎖競爭并不是只是在 GCD 和隊列下才變得特殊,任何使用了鎖機(jī)制的程序都會碰到同樣的問題——只不過不同的鎖機(jī)制會以不同的方式碰到。
所有對dispatch_async,dispatch_sync等等的調(diào)用都需要完成某種形式的鎖——以確保僅有一個線程或者特定的線程運(yùn)行指定的代碼。GCD 某些程序上可以使用時序(譯注:原詞為 scheduling)來避免使用鎖,但在最后,問題只是稍有變化。根本問題仍然存在:如果你有大量的線程在相同時間去訪問同一個鎖或者隊列,你就會看到性能的變化。性能會嚴(yán)重下降。
你應(yīng)該從直接復(fù)雜層次中隔離開。當(dāng)你發(fā)現(xiàn)了性能下降,這明顯表明代碼中存在設(shè)計問題。這里有兩個開銷需要你來平衡。第一個是獨(dú)占臨界區(qū)資源太久的開銷,以至于別的線程都因為進(jìn)入臨界區(qū)的操作而阻塞。第二個是太頻繁出入臨界區(qū)的開銷。在 GCD 的世界里,第一種開銷的情況就是一個 block 在隔離隊列中運(yùn)行,它可能潛在的阻塞了其他將要在這個隔離隊列中運(yùn)行的代碼。第二種開銷對應(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。
全都使用異步分發(fā)
我們在這稍稍轉(zhuǎn)變以下話題。正如你在上面看到的,你可以同步和異步地分發(fā)一個 block,一個工作單元。我們在《并發(fā)編程:API 及挑戰(zhàn)》)中討論的一個非常普遍的問題就是死鎖。在 GCD 中,以同步分發(fā)的方式非常容易出現(xiàn)這種情況。見下面的代碼:

一旦我們進(jìn)入到第二個dispatch_sync就會發(fā)生死鎖。我們不能分發(fā)到queueA,因為有人(當(dāng)前線程)正在隊列中并且永遠(yuǎn)不會離開。但是有更隱晦的產(chǎn)生死鎖方式:

單獨(dú)的每次調(diào)用dispatch_sync()看起來都沒有問題,但是一旦組合起來,就會發(fā)生死鎖。
這是使用同步分發(fā)存在的固有問題,如果我們使用異步分發(fā),比如:

一切運(yùn)行正常。
異步調(diào)用不會產(chǎn)生死鎖
。因此值得我們在任何可能的時候都使用異步分發(fā)。我們使用一個異步調(diào)用結(jié)果 block 的函數(shù),來代替編寫一個返回值(必須要用同步)的方法或者函數(shù)。這種方式,我們會有更少發(fā)生死鎖的可能性。
異步調(diào)用的副作用就是它們很難調(diào)試。當(dāng)我們在調(diào)試器里中止代碼運(yùn)行,回溯并查看已經(jīng)變得沒有意義了。
要牢記這些。死鎖通常是最難處理的問題。
如何寫出好的異步 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ù)年犃兄?,而且以這種方式編寫的代碼是很容易閱讀的??傊愕暮瘮?shù)將會(必須)調(diào)用dispatch_async()去運(yùn)行回調(diào)處理程序,所以它同時也可能在需要調(diào)用的隊列上做這些工作。
如果你寫一個類,讓你類的使用者設(shè)置一個回調(diào)處理隊列或許會是一個好的選擇。你的代碼可能像這樣:

如果你以這種方式來寫你的類,讓類之間協(xié)同工作就會變得容易。如果類 A 使用了類 B,它會把自己的隔離隊列設(shè)置為 B 的回調(diào)隊列。
迭代執(zhí)行
如果你正在倒弄一些數(shù)字,并且手頭上的問題可以拆分出同樣性質(zhì)的部分,那么dispatch_apply會很有用。
如果你的代碼看起來是這樣的:

小小的改動或許就可以讓它運(yùn)行的更快:

代碼運(yùn)行良好的程度取決于你在循環(huán)內(nèi)部做的操作。
block 中運(yùn)行的工作必須是非常重要的(注意),否則這個頭部信息就顯得過于繁重了。除非代碼受到計算帶寬的約束,每個工作單元為了很好適應(yīng)緩存大小而讀寫的內(nèi)存都是臨界的。這會對性能會帶來顯著的影響。受到臨界區(qū)約束的代碼可能不會很好地運(yùn)行。詳細(xì)討論這些問題已經(jīng)超出了這篇文章的范圍。使用dispatch_apply可能會對性能提升有所幫助,但是性能優(yōu)化本身就是個很復(fù)雜的主題。維基百科上有一篇關(guān)于Memory-bound function的文章。內(nèi)存訪問速度在 L2,L3 和主存上變化很顯著。當(dāng)你的數(shù)據(jù)訪問模式與緩存大小不匹配時,10倍性能下降的情況并不少見。
組
很多時候,你發(fā)現(xiàn)需要將異步的 block 組合起來去完成一個給定的任務(wù)。這些任務(wù)中甚至有些是并行的?,F(xiàn)在,如果你想要在這些任務(wù)都執(zhí)行完成后運(yùn)行一些代碼,"groups" 可以完成這項任務(wù)。看這里的例子:

需要注意的重要事情是,所有的這些都是非阻塞的。我們從未讓當(dāng)前的線程一直等待直到別的任務(wù)做完。恰恰相反,我們只是簡單的將多個 block 放入隊列。由于代碼不會阻塞,所以就不會產(chǎn)生死鎖。
同時需要注意的是,在這個小并且簡單的例子中,我們是怎么在不同的隊列間進(jìn)切換的。
對現(xiàn)有API使用 dispatchgroupt
一旦你將 groups 作為你的工具箱中的一部分,你可能會懷疑為什么大多數(shù)的異步API不把dispatch_group_t作為一個可選參數(shù)。這沒有什么無法接受的理由,僅僅是因為自己添加這個功能太簡單了,但是你還是要小心以確保自己使用 groups 的代碼是成對出現(xiàn)的。
舉例來說,我們可以給 Core Data 的-performBlock:API 函數(shù)添加上 groups,就像這樣:

當(dāng) Core Data 上的一系列操作(很可能和其他的代碼組合起來)完成以后,我們可以使用dispatch_group_notify來運(yùn)行一個 block 。
很明顯,我們可以給NSURLConnection做同樣的事情:

為了能正常工作,你需要確保:
? ? dispatch_group_enter()必須要在dispatch_group_leave()之前運(yùn)行。
? ? dispatch_group_enter()和dispatch_group_leave()一直是成對出現(xiàn)的(就算有錯誤產(chǎn)生時)。
事件源
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)的。
監(jiān)視進(jìn)程
如果一些進(jìn)程正在運(yùn)行而你想知道他們什么時候存在,GCD 能夠做到這些。你也可以使用 GCD 來檢測進(jìn)程什么時候分叉,也就是產(chǎn)生子進(jìn)程或者傳送給了進(jìn)程的一個信號(比如SIGTERM)。

當(dāng) Mail.app 退出的時候,這個程序會打印出Mail quit.。
注意:在所有的事件源被傳遞到你的事件處理器之前,必須調(diào)用dispatch_resume()。
監(jiān)視文件
這種可能性是無窮的。你能直接監(jiān)視一個文件的改變,并且當(dāng)改變發(fā)生時事件源的事件處理將會被調(diào)用。
你也可以使用它來監(jiān)視文件夾,比如創(chuàng)建一個watch folder:

你應(yīng)該總是添加DISPATCH_VNODE_DELETE去檢測文件或者文件夾是否已經(jīng)被刪除——然后就停止監(jiān)聽。
定時器
大多數(shù)情況下,對于定時事件你會選擇NSTimer。定時器的GCD版本是底層的,它會給你更多控制權(quán)——但要小心使用。
需要特別重點(diǎn)指出的是,為了讓 OS 節(jié)省電量,需要為 GCD 的定時器接口指定一個低的余地值(譯注:原文leeway value)。如果你不必要的指定了一個低余地值,將會浪費(fèi)更多的電量。
這里我們設(shè)定了一個5秒的定時器,并允許有十分之一秒的余地值:

取消
所有的事件源都允許你添加一個cancel handler。這對清理你為事件源創(chuàng)建的任何資源都是很有幫助的,比如關(guān)閉文件描述符。GCD 保證在cancel handle調(diào)用前,所有的事件處理都已經(jīng)完成調(diào)用。
參考上面的監(jiān)視文件例子中對dispatch_source_set_cancel_handler()的使用。
輸入輸出
寫出能夠在繁重的 I/O 處理情況下運(yùn)行良好的代碼是一件非常棘手的事情。GCD 有一些能夠幫上忙的地方。不會涉及太多的細(xì)節(jié),我們只簡單的分析下問題是什么,GCD 是怎么處理的。
習(xí)慣上,當(dāng)你從一個網(wǎng)絡(luò)套接字中讀取數(shù)據(jù)時,你要么做一個阻塞的讀操作,也就是讓你個線程一直等待直到數(shù)據(jù)變得可用,或者是做反復(fù)的輪詢。這兩種方法都是很浪費(fèi)資源并且無法度量。然而,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ù)塊:

如果你是在尋找 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)存緩存。
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)獨(dú)特的屬性是它可以基于零碎的內(nèi)存區(qū)域。這解決了我們剛提到的組合內(nèi)存的問題。當(dāng)你要將兩個數(shù)據(jù)對象連接起來時:

數(shù)據(jù)對象 c 并不會將 a 和 b 拷貝到一個單獨(dú)的,更大的內(nèi)存區(qū)域里去。相反,它只是簡單地 retain 了 a 和 b。你可以使用dispatch_data_apply來遍歷對象 c 持有的內(nèi)存區(qū)域:

類似的,你可以使用dispatch_data_create_subrange來創(chuàng)建一個不做任何拷貝操作的子區(qū)域。
讀和寫
在 GCD 的核心里,調(diào)度 I/O(譯注:原文為 Dispatch I/O) 與所謂的通道有關(guān)。調(diào)度 I/O 通道提供了一種與從文件描述符中讀寫不同的方式。創(chuàng)建這樣一個通道最基本的方式就是調(diào)用:

這將返回一個持有文件描述符的創(chuàng)建好的通道。在你通過它創(chuàng)建了通道之后,你不準(zhǔn)以任何方式修改這個文件描述符。
有兩種從根本上不同類型的通道:流和隨機(jī)存取。如果你打開了硬盤上的一個文件,你可以使用它來創(chuàng)建一個隨機(jī)存取的通道(因為這樣的文件描述符是可尋址的)。如果你打開了一個套接字,你可以創(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è)置一個接受連接的事件源:

當(dāng)接受了連接,我們創(chuàng)建一個I/O通道:

如果我們想要設(shè)置SO_KEEPALIVE(如果使用了HTTP的keep-alive),我們需要在調(diào)用dispatch_io_create前這么做。
創(chuàng)建好 I/O 通道后,我們可以設(shè)置讀取處理程序:

如果所有你想做的只是讀取或者寫入一個文件,GCD 提供了兩個方便的封裝:dispatch_read和dispatch_write。你需要傳遞給dispatch_read一個文件路徑和一個在所有數(shù)據(jù)塊讀取后調(diào)用的 block。類似的,dispatch_write需要一個文件路徑和一個被寫入的dispatch_data_t對象。
基準(zhǔn)測試
在 GCD 的一個不起眼的角落,你會發(fā)現(xiàn)一個適合優(yōu)化代碼的靈巧小工具:

把這個聲明放到你的代碼中,你就能夠測量給定的代碼執(zhí)行的平均的納秒數(shù)。例子如下:

在我的機(jī)器上輸出了:

也就是說添加1000個對象到 NSMutableArray 總共消耗了31803納秒,或者說平均一個對象消耗32納秒。
正如dispatch_benchmark的幫助頁面指出的,測量性能并非如看起來那樣不重要。尤其是當(dāng)比較并發(fā)代碼和非并發(fā)代碼時,你需要注意特定硬件上運(yùn)行的特定計算帶寬和內(nèi)存帶寬。不同的機(jī)器會很不一樣。如果代碼的性能與訪問臨界區(qū)有關(guān),那么我們上面提到的鎖競爭問題就會有所影響。
不要把它放到發(fā)布代碼中,事實上,這是無意義的,它是私有API。它只是在調(diào)試和性能分析上起作用。
訪問幫助界面:

原子操作
頭文件libkern/OSAtomic.h里有許多強(qiáng)大的函數(shù),專門用來底層多線程編程。盡管它是內(nèi)核頭文件的一部分,它也能夠在內(nèi)核之外來幫助編程。
這些函數(shù)都是很底層的,并且你需要知道一些額外的事情。就算你已經(jīng)這樣做了,你還可能會發(fā)現(xiàn)一兩件你不能做,或者不易做的事情。當(dāng)你正在為編寫高性能代碼或者正在實現(xiàn)無鎖的和無等待的算法工作時,這些函數(shù)會吸引你。
這些函數(shù)在atomic(3)的幫助頁里全部有概述——運(yùn)行man 3 atomic命令以得到完整的文檔。你會發(fā)現(xiàn)里面討論到了內(nèi)存屏障。查看維基百科中關(guān)于內(nèi)存屏障的文章。如果你還存在疑問,那么你很可能需要它。
計數(shù)器
OSAtomicIncrement和OSAtomicDecrement有一個很長的函數(shù)列表允許你以原子操作的方式去增加和減少一個整數(shù)值 —— 不必使用鎖(或者隊列)同時也是線程安全的。如果你需要讓一個全局的計數(shù)器值增加,而這個計數(shù)器為了統(tǒng)計目的而由多個線程操作,使用原子操作是很有幫助的。如果你要做的僅僅是增加一個全局計數(shù)器,那么無屏障版本的OSAtomicIncrement是很合適的,并且當(dāng)沒有鎖競爭時,調(diào)用它們的代價很小。
類似的,OSAtomicOr,OSAtomicAnd,OSAtomicXor的函數(shù)能用來進(jìn)行邏輯運(yùn)算,而OSAtomicTest可以用來設(shè)置和清除位。
10.2、比較和交換
OSAtomicCompareAndSwap能用來做無鎖的惰性初始化,如下:

如果沒有 buffer,我們會創(chuàng)建一個,然后原子地將其寫到buffer中如果buffer為NULL。在極少的情況下,其他人在當(dāng)前線程同時設(shè)置了buffer,我們簡單地將其釋放掉。因為比較和交換方法是原子的,所以它是一個線程安全的方式去惰性初始化值。NULL的檢測和設(shè)置buffer都是以原子方式完成的。
明顯的,使用dispatch_once()我們也可以完成類似的事情。
原子隊列
OSAtomicEnqueue()和OSAtomicDequeue()可以讓你以線程安全,無鎖的方式實現(xiàn)一個LIFO隊列(常見的就是棧)。對有潛在精確要求的代碼來說,這會是強(qiáng)大的代碼。
還有OSAtomicFifoEnqueue()和OSAtomicFifoDequeue()函數(shù)是為了操作FIFO隊列,但這些只有在頭文件中才有文檔 —— 閱讀他們的時候要小心。
自旋鎖
最后,OSAtomic.h頭文件定義了使用自旋鎖的函數(shù):OSSpinLock。同樣的,維基百科有深入的有關(guān)自旋鎖的信息。使用命令man 3 spinlock查看幫助頁的spinlock(3)。當(dāng)沒有鎖競爭時使用自旋鎖代價很小。
在合適的情況下,使用自旋鎖對性能優(yōu)化是很有幫助的。一如既往:先測量,然后優(yōu)化。不要做樂觀的優(yōu)化。
下面是 OSSpinLock 的一個例子:

就上面的例子而言,或許用不著這么麻煩,但它演示了一種理念。我們使用了ARC的__weak來確保一旦MyTableViewCell所有的實例都不存在,amountAttributes會調(diào)用dealloc。因此在所有的實例中,我們可以持有字典的一個單獨(dú)實例。
這段代碼運(yùn)行良好的原因是我們不太可能訪問到方法最里面的部分。這是很深奧的——除非你真正需要,不然不要在你的 App 中使用它。