當前位置:
首頁 > 最新 > iOS多線程全面解讀(四):鎖

iOS多線程全面解讀(四):鎖

寫在前面

多線程帶來的問題之一就是安全問題,「鎖」是為了使多個線程間可以相互排斥地使用全局變數等共享資源,簡單來說就是保證同一時刻只有一個線程訪問一塊代碼。

下面是一個有安全問題的代碼例子:

- (void)test {

//期望操作

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[self addNum];

});

//未預料的操作

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[self addNum];

});

}

static int i = 0;

- (void)addNum {

NSLog(@"執行開始 i=%d, 線程:%@", i, [NSThread currentThread]);

i++;

[NSThread sleepForTimeInterval:1]; //讓線程阻塞一秒

NSLog(@"執行結束 i=%d, 線程:%@", i, [NSThread currentThread]);

}

你只需要在你的主線程runloop中調用test就能測試了(如果目前看不明白關於線程的操作沒關係,這裡只是實現兩個任務並行,並且模擬延長了addNum這個方法的執行時間),我們看到列印結果:

看到了么,我們無法預料異常的調用何時開始,我們期望得到的i = 1(我們的期望線程是number=4),然而卻不一定能正常得到。所以,我們需要通過技術來實現共享變數的安全讀寫。

在介紹iOS的幾種鎖之前,先科普「死鎖」的概念。

死鎖:多線程等待一個永遠無法實現的條件而無法繼續執行。

1、NSLock

NSLock的使用非常簡單,只需要將需要加鎖的代碼全部放進lock和unlock方法中。

我們修改上面的addNum代碼如下:

//注意lock是一個NSLock類的全局變數lock=[NSLock new]

- (void)addNum {

[lock lock];

NSLog(@"執行開始 i=%d, 線程:%@", i, [NSThread currentThread]);

i++;

[NSThread sleepForTimeInterval:1]; //讓線程阻塞一秒

NSLog(@"執行結束 i=%d, 線程:%@", i, [NSThread currentThread]);

[lock unlock];

}

同樣運行程序調用test方法,列印如下:

作用一目了然吧,這裡我們的期望線程為number=5它完整的走了執行開始和執行結束,而沒有受到number=6線程的干擾(因為如果獲取不到鎖,number=6線程就休眠了)。

注意一:NSLock是基於POSIX線程實現的,lock和unlock都必須在同一個線程執行。

注意二:我們盡量將lock和unlock寫在一起,如果業務需要導致獲得鎖和解鎖的邏輯很分散(或者無法判斷是否在同一線程),可以調用-(BOOL)tryLock方法嘗試能否獲取該鎖,方便我們做不同的邏輯,代碼如下:

- (void)addNum {

if ([lock tryLock]) {

NSLog(@"得到鎖");

NSLog(@"執行開始 i=%d, 線程:%@", i, [NSThread currentThread]);

i++;

[NSThread sleepForTimeInterval:1]; //讓線程阻塞一秒

NSLog(@"執行結束 i=%d, 線程:%@", i, [NSThread currentThread]);

[lock unlock];

} else {

NSLog(@"沒有得到鎖");

}

}

2、NSConditionLock

顧名思義,帶條件的鎖。同樣實現了NSLocking協議,所以它的玩兒法和NSLock很像,而且它有一個方法很有意思。

lockWhenCondition:當線程A進入這裡的時候,調用該方法,若不滿足條件,線程就會進入休眠;若滿足條件,就會得到鎖並且執行下面的code。並且,若當前NSConditionLock的condition變為了滿足的時候,線程A又會蘇醒繼續執行。當然,需要和unlockWithCondition結合使用。

下面就是一個實現多個並發任務同步執行的例子:

NSConditionLock *clock = [[NSConditionLock alloc] initWithCondition:0];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[clock lockWhenCondition:0];

NSLog(@"任務1");

[NSThread sleepForTimeInterval:1];

[clock unlockWithCondition:2];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[clock lockWhenCondition:1];

NSLog(@"任務2");

[NSThread sleepForTimeInterval:1];

[clock unlockWithCondition:3];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[clock lockWhenCondition:2];

NSLog(@"任務3");

[NSThread sleepForTimeInterval:1];

[clock unlockWithCondition:1];

});

列印如下:

任務1

任務3

任務2

解析:我們三個任務中都使用了lockWhenCondition方法(注意這裡我用的是「任務」而非「線程」,「使用GCD線程的分配不是我們要關心的」),我們初始化鎖的時候用condition=0,所以先走任務1,任務1執行完畢調用[clock unlockWithCondition:2];所以接著走任務3,同理最後走任務2。

注意:如果大量使用條件鎖導致線程休眠,而開闢了過多的線程,將會對性能造成消耗,所以使用需謹慎??。

3、NSRecursiveLock

我們修改一下之前的例子,當線程1獲取到「鎖」過後,再次調用lock。

- (void)addNum {

[lock lock];

[lock lock];

NSLog(@"得到鎖");

NSLog(@"執行開始 i=%d, 線程:%@", i, [NSThread currentThread]);

i++;

[NSThread sleepForTimeInterval:1]; //讓線程阻塞一秒

NSLog(@"執行結束 i=%d, 線程:%@", i, [NSThread currentThread]);

[lock unlock];

}

運行結果就是造成了「死鎖」。還有一種常見的「死鎖」的情況是:A線程獲取到a鎖,B線程獲取到了b鎖,同一時刻,A線程想要獲取b鎖,B線程想要獲取a鎖,A、B線程就會同時進入休眠(這就尷尬了)。

為了解決以上代碼重複獲取鎖造成死鎖的情況,我們引入了遞歸鎖NSRecursiveLock。修改代碼如下:

//注意:recursiveLock是一個NSRecursiveLock類型的全局變數,recursiveLock = [NSRecursiveLock new]

- (void)addNum {

[recursiveLock lock];

[recursiveLock lock];

NSLog(@"得到鎖");

NSLog(@"執行開始 i=%d, 線程:%@", i, [NSThread currentThread]);

i++;

[NSThread sleepForTimeInterval:1]; //讓線程阻塞一秒

NSLog(@"執行結束 i=%d, 線程:%@", i, [NSThread currentThread]);

[recursiveLock unlock];

[recursiveLock unlock];

}

完美運行

注意一:使用NSRecursiveLock的時候,同樣得注意獲取鎖和解鎖需要一一對應,即一個lock對應一個unlock,不然鎖不會處於可獲取狀態(哈哈,嚴謹吧??,這裡是不可獲取狀態而不是釋放狀態)。

注意二:若你能確定排除被重複加鎖的情況,使用NSLock性能會更好。

4、@synchronized

@synchronized使用方法很簡單,修改代碼如下:

- (void)addNum {

@synchronized(self){

NSLog(@"得到鎖");

NSLog(@"執行開始 i=%d, 線程:%@", i, [NSThread currentThread]);

i++;

[NSThread sleepForTimeInterval:1]; //讓線程阻塞一秒

NSLog(@"執行結束 i=%d, 線程:%@", i, [NSThread currentThread]);

}

}

@synchronized(obj),obj參數一般指定為互斥鎖要保護的對象。值得提出的是,如果同一個obj對象做為了不同@synchronized(){}的參數,則這些代碼塊不能同時執行。

使用@synchronized我們不用管何時獲取鎖、何時釋放鎖,更容易的實現互斥,代碼更加直觀清晰;但是在性能上略顯不足,而且實現並行演算法有些複雜,不過這些都不能影響它極高的使用率。

ibireme大神的圖

寫在後面

「鎖」從來都不是為了炫技而生,大量使用鎖不僅會帶來性能問題,還會讓代碼更加的晦澀難懂,請大家合理的使用鎖。

iOS多線程全面解讀系列到這裡就結束了,希望給各位帶來了些正面的作用,當然文章我會持續跟進,減少文章中的遺漏和錯誤。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Cocoa開發者社區 的精彩文章:

TAG:Cocoa開發者社區 |