GCD實現單一資源的多讀單寫

在多線程編程中,最多見的場景是如何保證線程安全,好比你可能常常遇到多線程訪問某個dic(又或者是array或其餘)形成的crash。
這篇文章裏,咱們討論下如何使用GCD實現多線程讀者與寫者問題,也即單一資源的線程安全問題。
同時會有一些在MRC下crash問題討論。git

解決方案與原理

ARC版本github

_ioQueue = dispatch_queue_create("ioQueue", DISPATCH_QUEUE_CONCURRENT);
- (void)setSafeObject:(id)object forKey:(NSString *)key
{
    key = [key copy];
    dispatch_barrier_async(self.ioQueue, ^{
        if (key && object) {
            [_dic setObject:object forKey:key];
        }
    });
}
- (id)getSafeObjectForKey:(NSString *)key
{
    __block id result = nil;
    dispatch_sync(self.ioQueue, ^{
        result = [_dic objectForKey:key];
    });
    return result;
}
  • 首先,咱們須要建立一個私有的並行隊列來處理讀寫操做。
    在這裏不該該使用globe_queue, 由於咱們經過dispatch_barrier_async來保證寫操做的互斥,咱們不但願寫操做阻塞住globe_queue中的其餘不相關任務,咱們只但願在寫的同時,不會有其餘的寫操做或者讀操做。
    同時,也不推薦給隊列設置優先級,多數狀況下使用default就能夠了。而改變優先級每每會形成一些沒法預料的問題,好比優先級反轉(具體的能夠參看參考文獻)。編程

  • dispatch_barrier_async的block運行時機是,在它以前全部的任務執行完畢,而且在它後面的任務開始以前,期間不會有其餘的任務執行。注意在barrier執行的時候,隊列本質上如同一個串行隊列,其執行完之後纔會恢復到並行隊列。
    安全

    dispatch_barrier

  • 另一個值得注意的問題是,在寫操做的時候,咱們使用dispatch_async,而在讀操做的時候咱們使用dispatch_sync。很明顯,這2個操做一個是異步的,一個是同步的。咱們不須要使每次程序執行的時候都等待寫操做完成,因此寫操做異步執行,可是咱們須要同步的執行讀操做來保證程序可以馬上獲得它想要的值。
    多線程

  • 使用sync的時候須要極其的當心,由於稍不注意,就有可能產生死鎖,這可能形成災難性的後果。你確定也注意到了在寫操做的時候對key進行了copy, 關於此處的解釋,插入一段來自參考文獻的引用:併發

    函數調用者能夠自由傳遞一個NSMutableString的key,而且可以在函數返回後修改它。所以咱們必須對傳入的字符串使用copy操做以確保函數可以正確地工做。若是傳入的字符串不是可變的(也就是正常的NSString類型),調用copy基本上是個空操做。異步



到這裏整個基本示例代碼已經完成,通常狀況下可以知足咱們的須要。下面來看看在MRC過程當中我遇到的一些問題。async

關於死鎖

dispatch_queue_t queueA; // 串行隊列
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 = [key copy];
    dispatch_barrier_async(self.ioQueue, ^{
        if (key && object) {
            [_dic setObject:object forKey:key];
        }
    });
    [key release];
}
- (id)getSafeObjectForKey:(NSString *)key
{
    __block id result = nil;
    dispatch_sync(self.ioQueue, ^{
        result = [_dic objectForKey:key];
    });
    return result;
}

首先咱們看看上面這段代碼,基本就是ARC版本轉換過來的,看起來沒問題。那麼到底是不是真的沒問題,咱們跑段代碼試試看:oop

//版本一
- (void)test
{
    for (int i = 0; i < 1000000; i++) {             
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self setSafeObject:[NSString stringWithFormat:@"86+131633829%i", i] forKey:KEY];
        });
        NSString *result = [self getSafeObjectForKey: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 (int i = 0; i < 1000000; i++) {             
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self setSafeObject:[NSString stringWithFormat:@"86+131633829%i", i] forKey:KEY];
        });
        NSString *result = [[self getSafeObjectForKey:KEY] retain];
        NSLog(@"get string: %@, length : %lu", result, result.length);
        [result release];
    }
}

運行以後會發現,仍然會crash,其實問題和上面同樣,咱們的改動沒有真正的解決問題。最好的解決方案是在讀操做以前就已經retain住了,看看最終版的代碼吧:

//最終版
- (id)getSafeObjectForKey:(NSString *)key
{
    __block id result = nil;
    dispatch_sync(self.ioQueue, ^{
        result = [[_dic objectForKey:key] retain];
    });
    return [result autorelease];
}
注意retain過必定要釋放掉,否則或形成內存泄露。

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

轉載請註明出處哦,個人博客: luoyibu


GCD是一套很好用的多線程庫,更多的用法請看參考資料

參考資料

  1. 底層併發 API
  2. 併發編程:API 及挑戰
  3. GCD 深刻理解:第一部分
  4. GCD 深刻理解:第二部分
相關文章
相關標籤/搜索