本文由webfrogs譯自objc.io,原文做者Daniel Eggert。轉載請註明出處!html
本篇英文原文所發佈的站點objc.io是一個專門爲iOS和OS X開發者提供的深刻討論技術的平臺,文章含金量很高。這個平臺每個月發佈一次,每次都會有數篇文章針對同一個特殊的主題的不一樣方面來深刻討論。本月的主題是「併發編程」,本文翻譯的正是其中的第4篇文章。web
翻譯此文是受到了破船的啓發。他已經將objc.io本月主題的第二篇文章翻譯完成了。
《OC中併發編程的相關API和麪臨的挑戰(1)》
《OC中併發編程的相關API和麪臨的挑戰(2)》算法
首次翻譯文章,水平有限,歡迎指正。編程
一、從前。。。
二、延後執行
三、隊列
3.一、標記隊列
3.二、優先級
四、孤立隊列
4.一、資源保護
4.二、單一資源的多讀單寫
4.三、鎖競爭
4.四、全都使用異步分發
4.五、如何寫出好的異步API
五、迭代執行
六、組
6.一、對現有API使用dispatch_group_t
七、事件源
7.一、監視進程
7.二、監視文件
7.三、定時器
7.四、取消
八、輸入輸出
8.一、GCD和緩衝區
8.二、讀和寫
九、基準測試
十、原子操做
10.一、計數器
10.二、比較和交換
10.三、原子隊列
10.四、自旋鎖api
這篇文章裏,咱們將會討論一些iOS和OS X均可以使用的底層API。除了dispatch_once,咱們通常不鼓勵使用其中的任何一種技術。數組
可是咱們想要揭示出表面之下深層次的一些可利用的方面。這些底層的API提供了大量的靈活性,可是伴隨着靈活性而來的倒是程序複雜度的提高和咱們對代碼的更多責任。在咱們的文章《common background practices》中提到的高層次的API和模式可以讓你專一於手頭的任務而且免於大量的問題。而且一般來講,高層次的API會提供更好的性能,除非你能負擔的起使用底層API帶來的糾結和調試代碼的時間和努力。緩存
儘管如此,瞭解深層次下的軟件堆棧工做原理仍是有頗有幫助的。咱們但願這篇文章可以讓你更好的瞭解這個平臺,同時,讓你更加感謝這些高層的API。安全
首先,咱們將會分析大多數組成Grand Central Dispatch的部分。數年間,蘋果公司持續添加功能而且改善它。如今蘋果已經將其開源,這意味着它對其餘平臺也是可用的了。最後,咱們將會看一下原子操做——另外的一種底層構建代碼的集合。性能優化
也許,關於併發編程最好的書是M. Ben-Ari寫的《Principles of Concurrent Programming》ISBN 0-13-701078-8。若是你正在作任何有關併發編程的事情,你須要讀一下這本書。這本書已經寫了超過30年了,但仍然是沒法超越。簡潔的寫法,優秀的例子和練習,帶領你構建併發編程中的基本代碼塊。這本書如今已經絕版了,可是仍然有一些零散的複印本。有一個新版書,名字叫《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;
}
這段代碼僅僅只會運行一次。而且在連續調用這段代碼的期間,檢查操做是很高效的。你能使用它來初始化全局的數據好比單例。要注意到的是,使用dispatch_once_t會使得測試變得很是困難(單例和測試只能任取其一)。
要確保onceToken被聲明爲static,或者有全局做用域。任何其餘的狀況都會致使沒法預知的行爲。換句話說,不要把dispatch_once_t做爲一個對象的成員變量,或者相似的情形。
退回到遠古時代(其實也就是幾年前),人們會使用pthread_once,由於dispatch_once_t更容易使用而且不易出錯,因此你永遠都不會使用到pthread_once了。
另外一個常見的朋友就是dispatch_after了。它使工做延後執行。它是很強大的,可是要注意:你很容易就陷入到一堆麻煩中。通常的使用是這樣的:
- (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];
});
}
咋一看,這段代碼是極好的。可是這裏也存在一些缺點。咱們不能(直接)取消咱們已經提交到dispatch_after的代碼,它將會運行。
另一個須要注意的事情就是,在人們使用dispatch_after去完成工做時,容易寫出有時間bug的問題傾向的代碼。舉例來講,一些代碼運行的太早而且極可能不知道爲何,這時你把它放到了dispatch_after中,如今,全部代碼運行正常了。可是,幾周之後,程序中止工做了,而且因爲你沒有明確指出代碼是以什麼樣的次序運行的,調試代碼就變成了一場噩夢。永遠不要這樣作,大多數的狀況下,你最好把代碼放到正確的位置。若是代碼放到 -viewWillAppear 太早,那麼或許 -viewDidAppear 就是正確的地方。
你將會爲本身省去不少麻煩,經過在本身代碼中創建直接調用(相似-viewDidAppear)而不是依賴於 dispatch_after。
若是你須要一些事情在某個特定的時刻及時的運行,那麼 dispatch_after 或許會是個好的選擇。確保同時考慮了NSTimer,這個API雖然有點笨重,可是它容許你取消這個定時。
GCD的一個最基本的部分就是隊列。下面咱們會給出一些如何使用它的例子。當使用隊列的時候,給它們一個好的標籤會幫本身很多忙。當調試的時候,這個標籤會在Xcode(和lldb)中顯示,這會幫助你瞭解應用程序當前是由誰負責的:
- (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;
}
隊列能夠是並行也能夠是串行的。默認狀況下,他們是串行的,也就是說,任何給定的時間內,只能有一個單獨的代碼塊運行。這就是孤立隊列的運行方式。隊列也能夠是並行的,也就是同一時間內容許多代碼塊同時執行。
GCD隊列的內部使用的是線程。GCD管理這些線程,而且使用GCD的時候,你不須要本身建立線程。重要的外在部分是GCD提供給你的用戶API,一個不一樣的抽象層級的接口。當使用GCD來完成併發的工做時,你沒必要考慮線程方面的問題,代替的,只需考慮隊列和工做項目(提交給隊列的代碼)。固然在這些之下的,依然是線程在工做。GCD的抽象層次爲你一般的代碼編寫提供了更好的方式。
隊列和工做項目的方式同時解決了一個廣泛的會輻射出去的併發問題:若是咱們直接使用線程,而且想要作一些併發的事情,咱們可能將咱們的工做分紅100個小的工做項目,同時基於能夠利用的CPU內核數量來建立線程,姑且是8線程。咱們把這些工做項目送到這8個線程中。可是寫這個函數的人同時也想要使用併發,所以當你調用這個函數的時候,也會建立8個線程。如今,你有了 8×8=64 個線程,儘管你只有8個CPU核心,也就是說任什麼時候候只有12%的線程可以運行這時另外88%的線程什麼事情都沒作。使用GCD,你不會有這種問題,當操做系統關閉CPU核心以省電時,GCD甚至可以對應的調整線程數量。
GCD經過建立所謂的線程池來大體匹配CPU核心數量。要記住,線程的建立並非無代價的。每一個線程都須要佔用內存和內核資源。這裏也有一個問題:若是你提交了一個代碼塊給GCD,可是這個代碼塊阻塞了這個線程,那麼這個線程在這段時間內就不是可用的,而且不能及時處理其餘工做——它被阻塞了。爲了確保工做項目在隊列上一直是執行的,GCD不得不建立一個新的線程,並將新線程添加到線程池。
若是你的代碼正在阻塞許多線程,這回帶來很大的問題。最開始,線程消耗資源,更多的時,建立他們會變得代價高昂。這須要時間,並且在這段時間內,GCD沒法以全速來運行工做項目。有許多可以致使線程阻塞的事情,可是最多見的時與I/O操做有關,也就是從文件或者網絡中讀寫數據。正是由於這些緣由,你不該該在GCD隊列中以阻塞的方式來運行I/O操做。看一下下面的輸入輸出段落以瞭解如何以GCD良好運行的方式來進行I/O操做。
你可以爲你建立的任何一個隊列設置標記。這會是很強大的,而且有助於調試。
爲每個類建立本身的隊列而不是使用全局的隊列被認爲是一種好的方式。這種放肆下,你能夠設置隊列的名字,這讓調試變得輕鬆許多——Xcode可讓你在Debug Navigator中看到全部的隊列名字,或者你能夠直接使用lldb。(lldb) thread list命令將會在控制檯打印出全部隊列的名字。一旦你使用大量的異步內容,這是頗有價值的幫助。
使用私有隊列一樣強調封裝性。這時你本身的隊列,你要本身決定如何使用它。
默認狀況下,一個新建立的隊列轉發到默認優先級的全局隊列中。咱們就將會多討論一點有關優先級的東西。
你能夠改變你隊列轉發到的隊列——也就是說你能夠設置本身隊列的目標隊列。以這種方式,你能夠將不一樣隊列連接在一塊兒。你的類Foo的隊列轉發到類Bar的隊列,而類Bar的隊列又轉發到全局隊列。
當你使用孤立隊列(以後咱們也會討論)的時候,這會頗有用。Foo有一個孤立隊列,而且轉發到Bar的孤立隊列,考慮到Bar的孤立隊列所保護的資源,它會自動變爲線程安全的。
若是你但願多段代碼同時運行,那要確保你本身的隊列是併發的。同時須要注意,若是一個隊列的目標隊列使串行的(也就是非併發),那麼實際上這個隊列也會轉換爲一個串行隊列。
你經過設置目標爲全局隊列中的一個來改變本身隊列的優先級,可是你應該剋制這麼作的衝動。
在大多數狀況下,改變優先級不會使事情照你預想的方向運行。一些看起簡單的事情其實是一個很是複雜的問題。你很容易會碰到一個叫作優先級反轉的狀況。咱們的文章《Concurrent Programming: APIs and Challenges》(已經由破船翻譯了,詳見點擊此處)有更多關於這個問題的信息,這個問題幾乎致使了NASA的探路者火星漫遊器變成磚頭。
在此基礎上,使用DISPATCH_QUEUE_PRIORITY_BACKGROUND隊列時,你須要格外當心。除非你理解了throttled I/O and background status as per setpriority(2) (抱歉,我也沒理解,不知如何翻譯了。)的意思,不然不要使用它。 否則,系統可能會以難以忍受的方式終止你的應用程序的運行。這可能集中在處理I/O操做上,這種操做以一種不與系統其餘處理I/O操做的部分交互的方式運行。可是和優先級反轉結合起來,這回變成一種危險的狀況。
孤立隊列是GCD隊列使用中很是廣泛的一種模式。這裏有兩個變種。
多線程編程中,最多見的情形是你有一個資源,每次只有一個線程被容許訪問這個資源。
咱們在《有關併發編程的文章》(參考破船的譯文)中討論了資源在併發編程中意味着什麼,其實併發編程中的資源一般就是一塊內存或者一個對象,每次只有一個線程能夠訪問它。
舉例來講,咱們須要以多線程(或者多個隊列)方式訪問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的實例。
注意如下四點:
咱們可以改善上面的那個例子。GCD有可讓多線程運行的併發隊列。咱們可以安全地使用多線程來從NSMutableDictionary中讀取只要咱們不一樣時修改它。當咱們須要改變這個字典時,咱們使用barrier來分發這個塊代碼。這樣的塊代碼會在全部以前預約好的塊代碼完成以後執行,而且全部在它以後的塊都會在它完成後纔會執行。
咱們以如下方式建立隊列:
self.isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT);
而且用如下代碼來改變setter函數:
- (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);
}
});
}
當你使用併發隊列時,要確保全部的barrier調用都是async(異步)的。若是你使用dispatch_barrier_sync,那麼你極可能會使你本身(更確切的說是,你的代碼)產生死鎖。寫操做須要barrier,而且能夠是async的。
首先,這裏有一句警告:上面這個例子中咱們保護的資源是一個NSMutableDictionary,這段代碼做爲一個例子運行的不錯。可是在真實的代碼環境下,把孤立隊列放到一個正確的複雜度層級下是很重要的。
若是你對NSMutableDictionary的訪問操做變得很是頻繁,你會碰到一個已知的叫作鎖競爭的問題。鎖競爭並非只是在GCD和隊列下才變得特殊,任何使用了鎖機制的程序都會碰到一樣的問題——只不過不一樣的鎖機制會以不一樣的方式碰到。
全部對dispatch_async,dispatch_sync等等的調用都須要完成某種形式的鎖——以確保僅有一個線程或者特定的線程運行所給的塊代碼。GCD在一些範圍能夠避免使用鎖而以時序安排來代替,但在最後,問題只是指有所變化。根本問題仍然存在:若是你有大量的線程在同一時間去競爭同一個鎖,你就會看到性能的變化,性能會嚴重降低。
你應該從直接複雜層次中隔離開。當你發現了性能降低,這是代表代碼中,存在明顯的設計問題。這裏有兩個地方的開銷須要你來平衡。第一個是獨佔臨界區資源過久的開銷,以致於別的線程都從進入臨界區的操做中阻塞。第二個是太頻繁進出臨界區的開銷。在GCD的世界裏,第一種開銷的狀況就是一個塊代碼在孤立隊列中運行,它可能潛在的阻塞了其餘將要在這個孤立隊列中運行的代碼。第二種開銷對應的就是調用dispatch_async和dispatch_sync的開銷。不管再怎麼優化,這兩個動做都不是無代價的。
不幸的是,不存在通用的標準來講明什麼是正確的平衡,你須要本身評測和調整。
若是你看上面例子中的代碼,咱們的臨界區代碼僅僅作了很簡單的事情。這可能也可能不是好的,依賴於它怎麼被使用。
在你本身的代碼中,要考慮本身是否在更高的層次保護了孤立隊列。舉個例子,類Foo有一個孤立隊列而且它自己保護着本身訪問NSMutableDictionary,有可能有一個用到了Foo的類Bar有一個孤立隊列保護全部對類Foo的使用。換句話說,你須要把類Foo改變爲再也不是線程安全的(沒有孤立隊列),並在Bar中,使用一個孤立隊列來確保同一時間只能有一個線程使用Foo。
咱們在這稍稍轉變如下話題。正如你在上面看到的,你能夠分發一個塊,一個工做單元的方式,便可以是同步的,也能夠是異步的。咱們在關於併發API和陷阱的文章(能夠參考破船的譯文,見本文開頭)中討論最多的就是死鎖。在GCD中,以同步分發的方式很是容易出現這種狀況。見下面的代碼:
dispatch_queue_t queueA; // assume we have this
dispatch_sync(queueA, ^(){
dispatch_sync(queueA, ^(){
foo();
});
});
一旦咱們進入到第二個dispatch_sync,就會發生死鎖。咱們不能分發到queueA,由於有人(當前線程)正在隊列中而且永遠不會離開。可是有更隱晦的死鎖方式:
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();
});
}
單獨的每次調用dispatch_sync()看起來都沒有問題,可是一旦組合起來,就會發生死鎖。
這是使用同步分發存在的固有問題,若是咱們使用異步分發,好比:
dispatch_queue_t queueA; // assume we have this
dispatch_async(queueA, ^(){
dispatch_async(queueA, ^(){
foo();
});
});
一切運行正常。異步調用不會產生死鎖。所以值得咱們在任何可能的時候都使用異步分發。咱們使用一個異步調用結果塊的函數,來代替編寫一個返回值(這必需要用同步)的方法或者函數。這種方式,咱們會有更少發生死鎖的可能性。
異步調用的反作用就是它們很難調試。當咱們中止了調試器中的代碼,再回溯並查看已經變得沒有意義了。
要記住這些。死鎖一般是最難處理的問題。
若是你正在給設計一個給別人(或者是給本身)使用的API,你須要記住幾個好的實踐。
正如咱們剛剛提到的,你須要傾向於異步API。當你建立一個API,它會在你的控制以外以各類方式調用,若是你的代碼能產生死鎖,那麼死鎖就會發生。
若是你須要寫的函數或者方法,那麼讓它們調用dispatch_async()。不要讓你的函數調用者來這麼作,調用者應該能夠經過調用你提供的方法或者函數來作到這個。
若是你的方法或函數有一個返回值,經過一個回調的處理來異步傳遞返回值。這個API應該是這樣的,你的方法或函數持有一個結果塊代碼和一個將結果傳遞到的目標隊列。你函數的調用着不須要本身來將結果分發。這麼作的緣由很簡單:幾乎全部時間,調用者都須要在一個適當的隊列中,這種方式的代碼是很容易被閱讀的。而且你的函數不管如何將會(必須)調用dispatch_async()來進行回調處理。
若是你寫一個類,讓你類的使用這設置一個將回調傳遞到的隊列會是一個好的選擇。你的代碼可能像這樣:
- (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);
});
});
}
若是你以這種方式來寫你的類,讓類一塊兒工做就會變得至關容易。若是類A使用了類B,它會把本身的孤立隊列設置爲B的回調隊列。
若是你正在擺弄一些數字,而且手頭上的問題能夠拆分爲小的一樣的部分,那麼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
}
});
代碼運行更好的程度取決於你在循環內部作的操做。
block中運行的工做必定要是很是重要的,不然最外層的那個dispatch_apply的定義就顯得太繁瑣了。
除非代碼受到計算帶寬的約束,每一個工做單元讀寫所須要的合適的緩存大小所佔用內存是可有可無的,這對性能會帶來顯著的影響。受到臨界區約束的代碼可能不會運行良好。詳細討論這些問題已經超出了這篇文章的範圍。使用dispatch_apply可能會對性能提高有所幫助,可是性能優化自己是個很複雜的主題。維基百科上有一篇關於Memory-bound function的文章。內存訪問速度在L2,L3和主存上變化很大。當你的數據訪問模式與緩存大小不匹配時,10倍的性能降低的狀況並很多見。
不少時候,你發現須要將幾個異步代碼塊組合起來去完成一個給定的任務。這些任務中甚至有些是能夠並行的。如今,若是你想要在這些代碼塊都執行完成後運行一些代碼,「組」能夠完成這項任務。看這裏的例子:
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, ^(){
// 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);
});
須要注意到的重要的事情是,全部的這些都是非阻塞的。咱們從未讓當前的線程一直等待直到別的任務作完。偏偏相反,咱們只是簡單的將多個代碼塊放入隊列。因爲代碼不會阻塞,因此就不會產生死鎖。
同時須要注意的是,在這個小的簡單的例子中,咱們是怎麼在不一樣的隊列間進切換的。
一旦你將組做爲你的工具箱中的一部分,你可能會想知道爲何大多數的異步API不把dispatch_group_t做爲其的一個可選參數。這沒有什麼使人絕望的理由,僅僅是由於本身添加這個功能太簡單了,可是你仍是要當心以確保本身的代碼是成對出現的。
舉例來講,咱們能夠給Core Data的-performBlock:函數添加上組的功能,那麼API會變得像這個樣子:
- (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);
}];
}
}
這樣作容許咱們使用dispatch_group_notify來運行一段代碼,當Core Data上的一堆操做完成之後。
很明顯,咱們能夠給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);
}];
}
}
爲了能正常工做,你須要確保:
GCD有一個較少人知道的特性:事件源dispatch_source_t。
正如大多數的GCD,它也是很底層的。當你須要用到它時,它會變得極其有用。它的一些使用是祕傳招數,咱們將會接觸到一部分。是事件源大部分對iOS平臺來講不是頗有用,由於在iOS平臺有諸多限制,你沒法啓動進程(所以就沒有必要監視進程),也不能在你的app以外寫數據(所以也就沒有必要去監視文件)等等。
GCD事件源是以極其資源高效的方式實現的。
若是一些進程正在運行而你想知道他們何時存在,GCD可以作到這些。你也可使用GCD來檢測進程何時分叉,也就是產生了子進程或者一個信號被傳送給了進程(好比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);
當Mail.app退出的時候,這個程序會打印出Mail quit.。
這種可能性是無窮盡的。你能直接監視一個文件的改變,而且當改變發生時,事件源的事件處理將會被調用。
你也可使用它來監視文件夾,好比建立一個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);
你應該一直添加DISPATCH_VNODE_DELETE去檢測文件或者文件夾是否已經被刪除——而後就中止監聽。
大多數狀況下,對於定時事件,你會選擇NSTimer。定時器的GCD版本是底層的,它會給你更多控制權——但要當心使用。
須要特別重點指出的是,爲了讓OS節省電量,須要爲GCD的定時器接口指定一個低的偏差值。若是你沒必要要的指定了一個太低的偏差值,你將會浪費更多的電量。
這裏咱們設定了一個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);
全部的事件源都容許你添加一個cancel handler。這對清理你爲事件源建立的任何資源都是頗有幫助的,好比關閉文件描述符。GCD保證在cancel handle調用前,全部的事件處理都已經完成調用。
看上面的監視文件例子中對dispatch_source_set_cancel_handler()的使用。
寫出可以在繁重的I/O處理狀況下運行良好的代碼是一件很是棘手的事情。GCD有一些可以幫上忙的地方。不會涉及太多的細節,咱們只簡單的分析下問題是什麼,GCD是怎麼處理的。
習慣上,當你從一個網絡套接字中讀取數據時,你要麼作一個阻塞的讀取操做,也就是讓你個線程一直等待直到數據變得可用,或者是作反覆的輪詢操做。這兩種方法都是很浪費資源而且沒法度量。然而,kqueue解決了輪詢的問題,經過當數據變得可用時傳遞一個事件,GCD也採用了一樣的方法,可是更加優雅。當向套接字寫數據時,一樣的問題也存在,這時你要麼作阻塞的寫操做,要麼等待套接字可以接收數據。
在處理I/O時,還有一個問題就是數據是以塊的形式到達的。當從網絡中讀取數據時,依據MTU(最大傳輸單元)數據塊典型的大小是在1.5K左右。這使得數據塊內能夠是任何內容。一旦數據到達,你一般只是對跨多個數據塊的內容感興趣。並且一般你會在一個大的緩衝區裏將數據組合起來而後再進行處理。假設(人爲例子)你收到了這樣8個數據塊:
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的頭部,將全部數據塊組合成一個大的緩衝區而且從中查找\r\n\r\n是很是簡單的。可是這樣作,你會大量地複製這些數據。大量舊的C語言API存在的一個問題就是,緩衝區沒有全部權的概念,因此函數不得不將數據再次拷貝到本身的緩衝區中——又一次的拷貝。拷貝數據操做看起來是可有可無的,可是當你正在作大量的I/O操做的時候,你會在你的profiling tool(Instruments)中看到這些拷貝操做大量出現。即便你僅僅每一個內存區域拷貝一次,你仍是使用了兩倍的存儲帶寬而且佔用了兩倍的內存緩存。
最直接了當的方法是使用數據緩衝區。GCD有一個dispatch_data_t類型,在某種程度上和Objective-C的NSData類型很類似。可是它能作別的事情,並且更通用。
注意,dispatch_data_t可以作retain和release操做,而且dispatch_data_t擁有它持有的對象。
這看起來可有可無,可是咱們必須記住GCD只是一個普通的C API,而且不能使用Objective-C。 一般的作法是建立一個緩衝區,這個緩衝區要麼是基於棧的,要麼是malloc操做分配的內存區域,這些都沒有全部權。
dispatch_data_t的一個獨特的屬性是它能夠基於零碎的內存區域。這解決了咱們剛提到的組合內存的問題。當你要將兩個數據對象鏈接起來時:
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);
數據對象c並不會將a和b拷貝到一個單獨的,更大的內存區域裏去。相反,它只是簡單地持有a和b。你可使用dispatch_data_apply來遍歷對象c持有的內存區域:
dispatch_data_apply(c, ^(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來建立一個不作任何拷貝操做的子區域。
在GCD的內核中,Dispatch I/O就是所謂的通道。調度I/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));
這將返回一個持有文件描述符的建立好的通道。在你經過它建立了通道以後,你不許以任何方式修改這個文件描述符。
有兩種從根本上不一樣類型的通道:流和隨機存取。若是你打來了硬盤上的一個文件,你可使用它來建立一個隨機存取的通道(由於這樣的文件描述符是可尋址的)。若是你打開了一個套接字,你能夠建立一個流通道。
若是你想要爲一個文件建立一個通道,你最好使用須要一個路徑參數的dispatch_io_create_with_path,而且讓GCD來打開這個文件。這時有利的,由於GCD可以延遲打開這個文件,以限制同一時間同時打開的文件數量。
相似一般的read(2),write(2)和close(2)的操做,GCD提供了dispatch_io_read,dispatch_io_write和dispatch_io_close。不管什麼時候數據被讀完或者寫完,讀寫操做經過調用一個回調塊來結束。這些都是以非阻塞,異步I/O的形式高效實現的。
在這你得不到全部的細節,可是這裏會提供一個建立TCP服務端的例子:
首先咱們建立一個監聽套接字,而且設置一個接收鏈接的事件源:
_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);
});
當接受了鏈接,咱們建立一個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;
若是咱們想要設置SO_KEEPALIVE(若是咱們使用了HTTP的keep-alive等級),咱們須要在調用dispatch_io_create前這麼作。
建立好I/O通道後,咱們能夠設置讀取處理程序:
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一個文件路徑和一個在全部數據塊讀取完後調用的代碼塊。相似的,dispatch_write須要一個文件路徑和一個被寫入的dispatch_data_t對象。
在GCD的一個不起眼的角落,你會發現一個適合優化代碼的靈巧小工具:
uint64_t dispatch_benchmark(size_t count, void (^block)(void));
把這個聲明放到你的代碼中,你就可以測量給定的代碼塊執行的平均的納秒數。例子以下:
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的幫助界面指出的,測量性能並不是如看起來那樣不重要。尤爲是當比較併發代碼和非併發代碼時,你須要注意你的硬件上運行的特定計算帶寬和內存帶寬。不一樣的機器會很不同。若是代碼的性能與訪問臨界區有關,那麼咱們上面提到的鎖競爭問題就會有所影響。
不要把它放到發佈代碼中,事實上,這是無心義的,它是私有API。它只是在調試和性能分析上起做用。
訪問幫助界面:
curl "http://opensource.apple.com/source/libdispatch/libdispatch-84.5/man/dispatch_benchmark.3?txt"
| /usr/bin/groffer --tty -T utf8
頭文件libkern/OSAtomic.h裏有許多強大的函數,專門用來底層多線程編程。儘管它是內核頭文件的一部分,它也可以在內核以外來幫助編程。
這些函數都是很底層的,而且你須要知道一些額外的事情。就算你已經知道了,你還可能會發現一兩件你不能作,或者不易作的事情。當你正在爲高性能代碼工做或者正在實現無鎖的和無等待的算法工做時,這些函數會變得頗有趣。
這些函數在atomic(3)的幫助頁裏所有有概述——運行man 3 atomic命令以獲得完整的文檔。你會發現裏面討論到了內存屏障。查看維基百科中關於內存屏障的文章。若是你不能肯定,那麼你極可能須要它。
OSAtomicIncrement和OSAtomicDecrement有一個很長的函數列表容許你以原子操做的方式去增長和減小一個整數值,這沒必要使用鎖(或者隊列)同時也是線程安全的。若是你須要讓一個全局的計數器值增長,而這個計數器爲了統計目的而由多個線程操做,使用原子操做是頗有幫助的。若是你要作的僅僅是增長一個全局計數器,那麼無屏障版本的OSAtomicIncrement是很合適的,而且當沒有鎖競爭時,調用它們的代價很小。
相似的,OSAtomicOr,OSAtomicAnd,OSAtomicXor的函數能用來進行邏輯運算,而OSAtomicTest能夠用來設置和清除位。
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中若是buffer爲NULL。在極稀少的狀況下,其餘人由於多線程也同時設置了buffer,咱們簡單的將其釋放掉。由於比較和交換方法是原子的,因此它是一個線程安全的方式去懶初始化值。NULL的檢測和設置buffer是以原子方式完成的。
明顯的,使用dispatch_once()咱們也能夠完成相似的事情。
OSAtomicEnqueue()和OSAtomicDequeue()可讓你實現一個LIFO隊列,以線程安全,無鎖的方式。對有潛在精確要求的代碼來講,這會是強大的構建方式。
還有OSAtomicFifoEnqueue()和OSAtomicFifoDequeue()是爲了操做FIFO隊列,但這些只有在頭文件中才有文檔——使用他們的時候要當心。
最後,OSAtomic.h頭文件定義了使用自旋鎖的函數:OSSpinLock。再次的,維基百科有深刻的有關自旋鎖的信息。使用命令man 3 spinlock查看幫助頁的spinlock(3)。當沒有鎖競爭時使用自旋鎖代價很小。
在合適的狀況下,使用自旋鎖對性能優化是頗有幫助的。一如既往:先測量,而後優化。不要作樂觀的優化。
下面是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會調用dealloc。所以在全部的實例中,咱們能夠持有字典的一個單獨實例。
這段代碼運行良好的緣由是咱們不太可能訪問到函數內部的部分。這是很深奧的——不要在你的App中使用它,除非是你真正須要。