GCD 多執行緒安全 單寫多讀

weixin_33936401發表於2017-02-22

首先我們從一到執行緒安全的題目進入

2581808-baa23ce13a6bf1b8.png
有人問我這個題目,其實這就是在考多執行緒情況下,多讀一寫的解決方案

解決方案與原理

ARC版本

_ioQueue = dispatch_queue_create("ioQueue", DISPATCH_QUEUE_CONCURRENT);

- (void)setSafeObject:(id)object forKey:(NSString*)key

{

key = [keycopy];

dispatch_barrier_async(self.ioQueue, ^{if(key && object) {

[_dic setObject:object forKey:key];

}

});

}

- (id)getSafeObjectForKey:(NSString*)key

{

__blockidresult = nil;dispatch_sync(self.ioQueue, ^{

result = [_dic objectForKey:key];

});returnresult;

}

首先,我們需要建立一個私有的並行佇列來處理讀寫操作。

在這裡不應該使用globe_queue, 因為我們通過dispatch_barrier_async來保證寫操作的互斥,我們不希望寫操作阻塞住globe_queue中的其他不相關任務,我們只希望在寫的同時,不會有其他的寫操作或者讀操作。

同時,也不推薦給佇列設定優先順序,多數情況下使用default就可以了。而改變優先順序往往會造成一些無法預料的問題,比如優先順序反轉(具體的可以參看參考文獻)。

dispatch_barrier_async的block執行時機是,在它之前所有的任務執行完畢,並且在它後面的任務開始之前,期間不會有其他的任務執行。注意在barrier執行的時候,佇列本質上如同一個序列佇列,其執行完以後才會恢復到並行佇列。


另外一個值得注意的問題是,在寫操作的時候,我們使用dispatch_async,而在讀操作的時候我們使用dispatch_sync。很明顯,這2個操作一個是非同步的,一個是同步的。我們不需要使每次程式執行的時候都等待寫操作完成,所以寫操作非同步執行,但是我們需要同步的執行讀操作來保證程式能夠立刻得到它想要的值。

另外一個值得注意的問題是,在寫操作的時候,我們使用dispatch_async,而在讀操作的時候我們使用dispatch_sync。很明顯,這2個操作一個是非同步的,一個是同步的。我們不需要使每次程式執行的時候都等待寫操作完成,所以寫操作非同步執行,但是我們需要同步的執行讀操作來保證程式能夠立刻得到它想要的值。

使用sync的時候需要極其的小心,因為稍不注意,就有可能產生死鎖,這可能造成災難性的後果。你肯定也注意到了在寫操作的時候對key進行了copy, 關於此處的解釋,插入一段來自參考文獻的引用:

函式呼叫者可以自由傳遞一個NSMutableString的key,並且能夠在函式返回後修改它。因此我們必須對傳入的字串使用copy操作以確保函式能夠正確地工作。如果傳入的字串不是可變的(也就是正常的NSString型別),呼叫copy基本上是個空操作。

到這裡整個基本示例程式碼已經完成,一般情況下能夠滿足我們的需要。下面來看看在MRC過程中我遇到的一些問題。

關於死鎖

dispatch_queue_tqueueA;// 序列佇列dispatch_sync(queueA, ^(){dispatch_sync(queueA, ^(){

foo();

});

});

造成死鎖比較常見的情況可以簡化成上面這段程式碼。

dispatch_sync會同步的提交工作並在返回前等待其完成。第一dispatch_sync正在執行並等待它的block完成,但是block不能夠完成,它呼叫了第二個dispatch_sync,而第二個dispatch_sync會等待序列佇列中已經存在的第一個任務完成,很明顯這個任務無法完成,造成死鎖。

值得注意的是main_queue就是一個序列佇列。

MRC下容易遇到的問題與解決方案

- (void)setSafeObject:(id)object forKey:(NSString*)key

{

key = [keycopy];

dispatch_barrier_async(self.ioQueue, ^{if(key && object) {

[_dic setObject:object forKey:key];

}

});

[key release];

}

- (id)getSafeObjectForKey:(NSString*)key

{

__blockidresult = nil;dispatch_sync(self.ioQueue, ^{

result = [_dic objectForKey:key];

});returnresult;

}

首先我們看看上面這段程式碼,基本就是ARC版本轉換過來的,看起來沒問題。那麼究竟是不是真的沒問題,我們跑段程式碼試試看:

//版本一- (void)test

{for(inti =0; i <1000000; i++) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

[selfsetSafeObject:[NSStringstringWithFormat:@"86+131633829%i", i] forKey:KEY];

});NSString*result = [selfgetSafeObjectForKey:KEY];NSLog(@"get string: %@, length : %lu", result, result.length);

}

}

test執行後,很快就會發生crash,讀操作的result會發生野指標。

如果你有經驗的話,可能會發現問題:

如果某個執行緒a剛取出了result值,這次執行緒b開始執行寫操作,造成執行緒a中的result值成為了一份過期的資料,如果正好執行緒b的runloop結束,很有可能舊的result記憶體地址被釋放掉,這時執行緒a中的result就會發生野指標crash。

這時候,你可能會採取這樣子的修改,程式碼如下:

//版本二- (void)test

{for(inti =0; i <1000000; i++) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{

[selfsetSafeObject:[NSStringstringWithFormat:@"86+131633829%i", i] forKey:KEY];

});NSString*result = [[selfgetSafeObjectForKey:KEY] retain];NSLog(@"get string: %@, length : %lu", result, result.length);

[result release];

}

}

執行之後會發現,仍然會crash,其實問題和上面一樣,我們的改動沒有真正的解決問題。最好的解決方案是在讀操作之前就已經retain住了,看看最終版的程式碼吧:

//最終版- (id)getSafeObjectForKey:(NSString*)key

{

__blockidresult = nil;dispatch_sync(self.ioQueue, ^{

result = [[_dic objectForKey:key] retain];

});return[result autorelease];

}

注意retain過一定要釋放掉,不然或造成記憶體洩露。

再次驗證後發現,程式不會crash了。

GCD是一套很好用的多執行緒庫,更多的用法請看參考資料

相關文章