程式設計師用有限的生命去追求無限的知識。
有言在先
首先我不是故意要做標題黨的,也不是我要炒冷飯,我只是想換個姿勢看多執行緒,本文大部分內容在分析如何造死鎖,奈何功力尚淺,然而再淺,也需要走出第一步。開啟你的 Xcode 來驗證這些死鎖吧。
多執行緒小知識
以下是實現多執行緒的三種方式:
- NSThread
- GCD
- NSOperationQueue
關於具體使用的方法不再具體介紹,讓我們來看看他們不為人知的一面
1. 鎖的背後
NSLock
是基於 POSIX threads 實現的,而 POSIX threads 中使用互斥量同步執行緒。
互斥量(或稱為互斥鎖)是 pthread 庫為解決這個問題提供的一個基本的機制。互斥量是一個鎖,它保證如下三件事情:
-
原子性 - 鎖住一個互斥量是一個原子操作,表明作業系統保證如果在你已經鎖了一個互斥量,那麼在同一時刻就不會有其他執行緒能夠鎖住這個互斥量;
-
奇異性 - 如果一個執行緒鎖住了一個互斥量,那麼可以保證的是在該執行緒釋放這個鎖之前沒有其他執行緒可以鎖住這個互斥量;
-
非忙等待 - 如果一個執行緒(執行緒1)嘗試去鎖住一個由執行緒2鎖住的鎖,執行緒1會掛起(suspend)並且不會消耗任何CPU資源,直到執行緒2釋放了這個鎖。這時,執行緒1會喚醒並繼續執行,鎖住這個互斥量。
2. 關於生命週期
通過 [NSThread exit]
方法使執行緒退出 ,NSThread
是可以立即終止正在執行的任務(可能會造成記憶體洩露,這裡不深究)。甚至你可以在主執行緒中執行該操作,會使主執行緒也退出,app 無法再響應事件。而 cancel
可以通過作為標誌位來達到類似目的,如果不做任何處理,仍然會繼續執行。
GCD和NSOperationQueue可以取消佇列中未開始執行的任務,對於已經開始執行的任務就無能為力了。
實現方式\功能 | 執行緒生命週期 | 取消任務 |
---|---|---|
NSThread | 手動管理 | 立即停止執行 |
GCD | 自動管理 | 取消佇列中未執行的任務 |
NSOperationQueue | 自動管理 | 取消佇列中未執行的任務 |
3. 並行與併發
看到很多文章裡提到 併發佇列 ,這裡有一個小陷阱,混淆了 併發 和 並行 的概念。我們先來看看一下他們之間的區別:
從圖中可以看到,並行才是真正的多執行緒,而併發只是在多工中切換。一般多核CPU可以並行執行多個執行緒,而單核CPU實際上只有一個執行緒,多路複用達到接近同時執行的效果。在 iOS 中 dispatch_async 和 globalQueue 從 Xcode 中執行緒使用情況來看,都達到了並行的效果。
4. 佇列與執行緒
佇列是儲存以及管理任務的,將任務加到佇列中,任務會按照加入到佇列中先後順序依次執行。如果是全域性佇列和併發佇列,則系統會根據系統資源去建立新的執行緒去處理佇列中的任務,執行緒的建立、維護和銷燬由作業系統管理,還有佇列本身是執行緒安全的。
使用 NSOperationQueue 實現多執行緒的時候是可以控制執行緒總數及執行緒依賴關係的,而 GCD 只能選擇併發或者序列佇列。
資源競爭
多執行緒同時執行任務能提高程式的執行效率和響應時間,但是多執行緒不可避免地遇到同時操作同一資源的情況。前段時間看到的一個資源競爭的問題為例:
@property (nonatomic, strong) NSString *target;
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
});
}
複製程式碼
解決辦法:
@property (nonatomic, strong) NSString *target;
將nonatomic
改成atomic
。- 將併發佇列
DISPATCH_QUEUE_CONCURRENT
改成序列佇列DISPATCH_QUEUE_SERIAL
。 - 非同步執行
dispatch_async
改成同步執行dispatch_sync
。 - 賦值使用
@synchronized
或者上鎖。
這些方法都是從避免同時訪問的角度來解決該問題,有更好的方法歡迎分享。
花樣死鎖
任何事情都有兩面性,就像多執行緒能提升效率的同時,也會造成資源競爭的問題。而鎖在保證多執行緒的資料安全的同時,粗心大意之下也容易發生問題,那就是 死鎖 。
1. NSOperationQueue
鑑於 NSOperationQueue 高度封裝,使用起來非常簡單,一般不會出什麼么蛾子,下面的案例展示了一個不好示範,通常我們通過控制 NSOperation 之間的從屬關係,來達到有序執行任務的效果,但是如果互相從屬或者迴圈從屬都會造成所有任務無法開始。
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"lock 1 start");
[NSThread sleepForTimeInterval:1];
NSLog(@"lock 1 over");
}];
NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"lock 2 start");
[NSThread sleepForTimeInterval:1];
NSLog(@"lock 2 over");
}];
NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"lock 3 start");
[NSThread sleepForTimeInterval:1];
NSLog(@"lock 3 over");
}];
// 迴圈從屬
[blockOperation2 addDependency:blockOperation1];
[blockOperation3 addDependency:blockOperation2];
[blockOperation1 addDependency:blockOperation3]; // 迴圈的罪魁禍首
// 互相從屬
//[blockOperation1 addDependency:blockOperation2];
//[blockOperation2 addDependency:blockOperation1];
[_operationQueue addOperation:blockOperation1];
[_operationQueue addOperation:blockOperation2];
[_operationQueue addOperation:blockOperation3];
複製程式碼
有沒有人試過下面這種情況,如果好奇就試試吧!
[blockOperation1 addDependency:blockOperation1];
複製程式碼
2. GCD
大多數開發者都知道在主執行緒裡同步執行任務會造成死鎖,一起來看看還有哪些情況下會造成死鎖或類似問題。
a. 在主執行緒同步執行 造成 EXC_BAD_INSTRUCEION
錯誤:
- (void)deadlock1 {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"task 1 start");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"task 1 over");
});
}
複製程式碼
b. 和主執行緒同步執行類似,在序列佇列中巢狀使用同步執行任務,同步佇列 task1 執行完成後才能執行 task2 ,而 task1 中巢狀了task2 導致 task1 註定無法完成。
- (void)deadlock2 {
dispatch_queue_t queue = dispatch_queue_create("com.xietao3.sync", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{ // 此處非同步同樣會造成互相等待
NSLog(@"task 1 start");
dispatch_sync(queue, ^{
NSLog(@"task 2 start");
[NSThread sleepForTimeInterval:1.0];
NSLog(@"task 2 over");
});
NSLog(@"task 1 over");
});
}
複製程式碼
巢狀同步執行任務確實很容易出 bug ,但不是絕對,將同步佇列DISPATCH_QUEUE_SERIAL
換成併發佇列 DISPATCH_QUEUE_CONCURRENT
這個問題就迎刃而解。修改成併發佇列後案例中 task1 仍然要先執行完巢狀在其中的 task2 ,而 task2 開始執行時,併發佇列不會發生互相等待導致阻塞問題 , task2 執行完成後 task1 繼續執行。
c. 在很多人印象中,非同步執行不容易發生互相等待的情況,確實,即使是序列佇列,非同步任務會等待當前任務執行後再開始,除非你加了一些不健康的佐料。
- (void)deadlock3 {
dispatch_queue_t queue = dispatch_queue_create("com.xietao3.asyn", DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
__block NSString *str = @"xietao3"; // 執行緒1 建立資料
dispatch_async(queue, ^{
str = [NSString stringWithFormat:@"%ld",[str hash]]; // 執行緒2 加工資料
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",str); // 執行緒1 使用加工後的資料
});
}
複製程式碼
d. 常規死鎖,在已經上鎖的情況下再次上鎖,形成彼此等待的局面。
if (!_lock) _lock = [NSLock new];
dispatch_queue_t queue = dispatch_queue_create("com.xietao3.sync", DISPATCH_QUEUE_CONCURRENT);
[_lock lock];
dispatch_sync(queue, ^{
[_lock lock];
[NSThread sleepForTimeInterval:1.0];
[_lock unlock];
});
[_lock unlock];
複製程式碼
要解決也比較簡單,將NSLock
換成遞迴鎖NSRecursiveLock
,遞迴鎖就像普通的門鎖,順時針轉一圈加鎖後,逆時針一圈即解鎖;而如果順時針兩圈,同樣逆時針兩圈即可解鎖。下面來一個遞迴的例子:
// 以下程式碼可以理解為順時針轉10圈上鎖,逆時針轉10圈解鎖
- (void)recursivelock:(int)count {
if (count>10) return;
count++;
if (!_recursiveLock) _recursiveLock = [NSRecursiveLock new];
[_recursiveLock lock];
NSLog(@"task%d start",count);
[self recursivelock:count];
NSLog(@"task%d over",count);
[_recursiveLock unlock];
}
複製程式碼
3. 其他
除了上面提到的互斥鎖和遞迴鎖,其他的鎖還有:
- OSSpinLock(自旋鎖)
- pthread_mutex(OC中鎖的底層實現)
- NSConditionLock(條件鎖,對於新手更容易產生死鎖)
- NSCondition(條件鎖的底層實現)
- @synchronized(物件鎖)
大部分鎖觸發死鎖的情況和互斥鎖基本一致,NSConditionLock
使用起來會更加靈活,而自旋鎖雖然效能爆表,但是存在漏洞,希望瞭解更多關於鎖的知識可以點這裡,在看的同時不要忘記親自動手驗證一下,邊看邊寫邊驗證,記得更加深刻。
總結
關於多執行緒、鎖的文章已經爛大街了,本文儘可能地從新的角度來看問題,儘量不寫那些重複的內容,希望對你有所幫助,如果文中內容有誤,歡迎指出。
轉載請註明原文:juejin.im/post/59c13d…