目前在 iOS 和 OS X 中有兩套先進(jìn)的同步 API 可供我們使用:NSOperation 和 GCD 。其中 GCD 是基于 C 的底層的 API ,而 NSOperation 則是 GCD 實(shí)現(xiàn)的 Objective-C API。 雖然 NSOperation 是基于 GCD 實(shí)現(xiàn)的, 但是并不意味著它是一個(gè) GCD 的 “dumbed-down” 版本, 相反,我們可以用NSOperation 輕易的實(shí)現(xiàn)一些 GCD 要寫大量代碼的事情。 因此, NSOperationQueue 是被推薦使用的, 除非你遇到了 NSOperationQueue 不能實(shí)現(xiàn)的問(wèn)題。
- 為什么優(yōu)先使用NSOperationQueue而不是GCD
曾經(jīng)我有一段時(shí)間我非常喜歡使用GCD來(lái)進(jìn)行并發(fā)編程,因?yàn)殡m然它是C的api,但是使用起來(lái)卻非常簡(jiǎn)單和方便, 不過(guò)這樣也就容易使開(kāi)發(fā)者忘記并發(fā)編程中的許多注意事項(xiàng)和陷阱。
比如你可能寫過(guò)類似這樣的代碼(這樣來(lái)請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)):
dispatch_async(_Queue, ^{
//請(qǐng)求數(shù)據(jù)
NSData *data = [NSData dataWithContentURL:[NSURL URLWithString:@"http://domain.com/a.png"]];
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshViews:data];
});
});
沒(méi)錯(cuò),它是可以正常的工作,但是有個(gè)致命的問(wèn)題:這個(gè)任務(wù)是無(wú)法取消的 dataWithContentURL:是同步的拉取數(shù)據(jù),它會(huì)一直阻塞線程直到完成請(qǐng)求,如果是遇到了超時(shí)的情況,它在這個(gè)時(shí)間內(nèi)會(huì)一直占有這個(gè)線程;在這個(gè)期間并發(fā)隊(duì)列就需要為其他任務(wù)新建線程,這樣可能導(dǎo)致性能下降等問(wèn)題。
因此我們不推薦這種寫法來(lái)從網(wǎng)絡(luò)拉取數(shù)據(jù)。
操作隊(duì)列(operation queue)是由 GCD 提供的一個(gè)隊(duì)列模型的 Cocoa 抽象。GCD 提供了更加底層的控制,而操作隊(duì)列則在 GCD 之上實(shí)現(xiàn)了一些方便的功能,這些功能對(duì)于 app 的開(kāi)發(fā)者來(lái)說(shuō)通常是最好最安全的選擇。NSOperationQueue相對(duì)于GCD來(lái)說(shuō)有以下優(yōu)點(diǎn):
提供了在 GCD 中不那么容易復(fù)制的有用特性。
可以很方便的取消一個(gè)NSOperation的執(zhí)行
可以更容易的添加任務(wù)的依賴關(guān)系
提供了任務(wù)的狀態(tài):isExecuteing, isFinished.
名詞: 本文中提到的 “任務(wù)”, “操作” 即代表要再NSOperation中執(zhí)行的事情。
- Operation Queues的使用
2.1 NSOperationQueue
NSOperationQueue 有兩種不同類型的隊(duì)列:主隊(duì)列和自定義隊(duì)列。主隊(duì)列運(yùn)行在主線程之上,而自定義隊(duì)列在后臺(tái)執(zhí)行。在兩種類型中,這些隊(duì)列所處理的任務(wù)都使用 NSOperation 的子類來(lái)表述。
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主隊(duì)列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定義隊(duì)列
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
//任務(wù)執(zhí)行
}];
[queue addOperation:operation];
我們可以通過(guò)設(shè)置 maxConcurrentOperationCount 屬性來(lái)控制并發(fā)任務(wù)的數(shù)量,當(dāng)設(shè)置為 1 時(shí), 那么它就是一個(gè)串行隊(duì)列。主對(duì)列默認(rèn)是串行隊(duì)列,這一點(diǎn)和 dispatch_queue_t 是相似的。
2.2 NSOperation
你可以使用系統(tǒng)提供的一些現(xiàn)成的 NSOperation 的子類, 如 NSBlockOperation、 NSInvocationOperation 等(如上例子)。你也可以實(shí)現(xiàn)自己的子類, 通過(guò)重寫 main 或者 start 方法 來(lái)定義自己的 operations 。
使用 main 方法非常簡(jiǎn)單,開(kāi)發(fā)者不需要管理一些狀態(tài)屬性(例如 isExecuting 和 isFinished),當(dāng) main 方法返回的時(shí)候,這個(gè) operation 就結(jié)束了。這種方式使用起來(lái)非常簡(jiǎn)單,但是靈活性相對(duì)重寫 start 來(lái)說(shuō)要少一些, 因?yàn)閙ain方法執(zhí)行完就認(rèn)為operation結(jié)束了,所以一般可以用來(lái)執(zhí)行同步任務(wù)。
@implementation YourOperation
- (void)main
{
// 任務(wù)代碼 ...
}
@end
如果你希望擁有更多的控制權(quán),或者想在一個(gè)操作中可以執(zhí)行異步任務(wù),那么就重寫 start 方法, 但是注意:這種情況下,你必須手動(dòng)管理操作的狀態(tài), 只有當(dāng)發(fā)送 isFinished 的 KVO 消息時(shí),才認(rèn)為是 operation 結(jié)束
@implementation YourOperation
- (void)start
{
self.isExecuting = YES;
// 任務(wù)代碼 ...
} - (void)finish //異步回調(diào)
{
self.isExecuting = NO;
self.isFinished = YES;
}
@end
當(dāng)實(shí)現(xiàn)了start方法時(shí),默認(rèn)會(huì)執(zhí)行start方法,而不執(zhí)行main方法
為了讓操作隊(duì)列能夠捕獲到操作的改變,需要將狀態(tài)的屬性以配合 KVO 的方式進(jìn)行實(shí)現(xiàn)。如果你不使用它們默認(rèn)的 setter 來(lái)進(jìn)行設(shè)置的話,你就需要在合適的時(shí)候發(fā)送合適的 KVO 消息。
需要手動(dòng)管理的狀態(tài)有:
isExecuting 代表任務(wù)正在執(zhí)行中
isFinished 代表任務(wù)已經(jīng)執(zhí)行完成
isCancelled 代表任務(wù)已經(jīng)取消執(zhí)行
手動(dòng)的發(fā)送 KVO 消息, 通知狀態(tài)更改如下 :
[self willChangeValueForKey:@"isCancelled"];
_isCancelled = YES;
[self didChangeValueForKey:@"isCancelled"];
為了能使用操作隊(duì)列所提供的取消功能,你需要在長(zhǎng)時(shí)間操作中時(shí)不時(shí)地檢查 isCancelled 屬性, 比如在一個(gè)長(zhǎng)的循環(huán)中:
@implementation MyOperation
- (void)main
{
while (notDone && !self.isCancelled) {
// 任務(wù)處理
}
}
@end
- RunLoop
在cocoa中講到多線程,那么就不得不講到RunLoop。 在ios/mac的編碼中,我們似乎不需要過(guò)多關(guān)心代碼是如何執(zhí)行的,一切仿佛那么自然。比如我們知道當(dāng)滑動(dòng)手勢(shì)時(shí),tableView就會(huì)滾動(dòng),啟動(dòng)一個(gè)NSTimer之后,timer的方法就會(huì)定時(shí)執(zhí)行, 但是為什么呢,其實(shí)是RunLoop在幫我們做這些事情:分發(fā)消息。
3.1 什么是RunLoop
你應(yīng)該看過(guò)這樣的偽代碼解釋ios的app中main函數(shù)做的事情:
int main(int argc, char * argv[])
{
while (true) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
也應(yīng)該看過(guò)這樣的代碼用來(lái)阻塞一個(gè)線程:
while (!complete) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
或許你感覺(jué)到他們有些神奇,希望我的解釋能讓你明白一些.
我們先思考一個(gè)問(wèn)題: 當(dāng)我們打開(kāi)一個(gè)IOS應(yīng)用之后,什么也不做,這時(shí)候看起來(lái)是沒(méi)有代碼在執(zhí)行的,為什么應(yīng)用沒(méi)有退出呢?
我們?cè)趯慶的簡(jiǎn)單的只有一個(gè)main函數(shù)的程序時(shí)就知道,當(dāng)main的代碼執(zhí)行完,沒(méi)有事情可做的時(shí)候,程序就執(zhí)行完畢退出了。而我們IOS的應(yīng)用是如何做到在沒(méi)有事情做的時(shí)候維持應(yīng)用的運(yùn)行的呢? 那就是RunLoop。
RunLoop的字面意思就是“運(yùn)行回路”,聽(tīng)起來(lái)像是一個(gè)循環(huán)。實(shí)際它就是一個(gè)循環(huán),它在循環(huán)監(jiān)聽(tīng)著事件源,把消息分發(fā)給線程來(lái)執(zhí)行。RunLoop并不是線程,也不是并發(fā)機(jī)制,但是它在線程中的作用至關(guān)重要,它提供了一種異步執(zhí)行代碼的機(jī)制。
3.2 事件源
runloop
由圖中可以看出NSRunLoop只處理兩種源:輸入源、時(shí)間源。而輸入源又可以分為:NSPort、自定義源、performSelector:OnThread:delay:, 下面簡(jiǎn)單介紹下這幾種源:
3.2.1 NSPort 基于端口的源
Cocoa和 Core Foundation 為使用端口相關(guān)的對(duì)象和函數(shù)創(chuàng)建的基于端口的源提供了內(nèi)在支持。Cocoa中你從不需要直接創(chuàng)建輸入源。你只需要簡(jiǎn)單的創(chuàng)建端口對(duì)象,并使用NSPort的方法將端口對(duì)象加入到run loop。端口對(duì)象會(huì)處理創(chuàng)建以及配置輸入源。
NSPort一般分三種: NSMessagePort(基本廢棄)、NSMachPort、 NSSocketPort。 系統(tǒng)中的NSURLConnection就是基于NSSocketPort進(jìn)行通信的,所以當(dāng)在后臺(tái)線程中使用NSURLConnection 時(shí),需要手動(dòng)啟動(dòng)RunLoop, 因?yàn)楹笈_(tái)線程中的RunLoop默認(rèn)是沒(méi)有啟動(dòng)的,后面會(huì)講到。
3.2.2 自定義輸入源
在Core Foundation程序中,必須使用CFRunLoopSourceRef類型相關(guān)的函數(shù)來(lái)創(chuàng)建自定義輸入源,接著使用回調(diào)函數(shù)來(lái)配置輸入源。Core Fundation會(huì)在恰當(dāng)?shù)臅r(shí)候調(diào)用回調(diào)函數(shù),處理輸入事件以及清理源。常見(jiàn)的觸摸、滾動(dòng)事件等就是該類源,由系統(tǒng)內(nèi)部實(shí)現(xiàn)。
一般我們不會(huì)使用該種源,第三種情況已經(jīng)滿足我們的需求
3.2.3 performSelector:OnThread
Cocoa提供了可以在任一線程執(zhí)行函數(shù)(perform selector)的輸入源。和基于端口的源一樣,perform selector請(qǐng)求會(huì)在目標(biāo)線程上序列化,減緩許多在單個(gè)線程上容易引起的同步問(wèn)題。而和基于端口的源不同的是,perform selector執(zhí)行完后會(huì)自動(dòng)清除出run loop。
此方法簡(jiǎn)單實(shí)用,使用也更廣泛。
3.2.4 定時(shí)源
定時(shí)源就是NSTimer了,定時(shí)源在預(yù)設(shè)的時(shí)間點(diǎn)同步地傳遞消息。因?yàn)門imer是基于RunLoop的,也就決定了它不是實(shí)時(shí)的。
3.3 RunLoop觀察者
我們可以通過(guò)創(chuàng)建CFRunLoopObserverRef對(duì)象來(lái)檢測(cè)RunLoop的工作狀態(tài),它可以檢測(cè)RunLoop的以下幾種事件:
Run loop入口
Run loop將要開(kāi)始定時(shí)
Run loop將要處理輸入源
Run loop將要休眠
Run loop被喚醒但又在執(zhí)行喚醒事件前
Run loop終止
3.4 Run Loop Modes
RunLoop對(duì)于上述四種事件源的監(jiān)視,可以通過(guò)設(shè)置模式來(lái)決定監(jiān)視哪些源。 RunLoop只會(huì)處理與當(dāng)前模式相關(guān)聯(lián)的源,未與當(dāng)前模式關(guān)聯(lián)的源則處于暫停狀態(tài)。
cocoa和Core Foundation預(yù)先定義了一些模式(Apple文檔翻譯):
Mode Name Description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 缺省情況下,將包含所有操作,并且大多數(shù)情況下都會(huì)使用此模式
Connection NSConnectionReplyMode (Cocoa) 此模式用于處理NSConnection的回調(diào)事件
Modal NSModalPanelRunLoopMode (Cocoa) 模態(tài)模式,此模式下,RunLoop只對(duì)處理模態(tài)相關(guān)事件
Event Tracking NSEventTrackingRunLoopMode (Cocoa) 此模式下用于處理窗口事件,鼠標(biāo)事件等
Common Modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 此模式用于配置”組模式”,一個(gè)輸入源與此模式關(guān)聯(lián),則輸入源與組中的所有模式相關(guān)聯(lián)。
我們也可以自定義模式,可以參考ASIHttpRequest在同步執(zhí)行時(shí),自定義了 runLoop 的模式叫 ASIHTTPRequestRunLoopMode。ASI的Timer源就關(guān)聯(lián)了此模式。
3.5 常見(jiàn)問(wèn)題一:為什么TableView滑動(dòng)時(shí),Timer暫停了?
我們做個(gè)測(cè)試: 在一個(gè) viewController 的 scrollViewWillBeginDecelerating: 方法里面打個(gè)斷點(diǎn), 然后滑動(dòng) tableView。 待斷點(diǎn)處, 使用 lldb 打印一下 [NSRunLoop currentRunLoop] 。 在描述中可以看到當(dāng)前的RunLoop的運(yùn)行模式:
current mode = UITrackingRunLoopMode
common modes = <CFBasicHash 0x14656e60 [0x3944dae0]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x398d54c0 [0x3944dae0]>{contents = "UITrackingRunLoopMode"}
1 : <CFString 0x39449d10 [0x3944dae0]>{contents = "kCFRunLoopDefaultMode"}
}
也就是說(shuō),當(dāng)前主線程的 RunLoop 正在以 UITrackingRunLoopMode 的模式運(yùn)行。 這個(gè)時(shí)候 RunLoop 只會(huì)處理與 UITrackingRunLoopMode “綁定”的源, 比如觸摸、滾動(dòng)等事件;而 NSTimer 是默認(rèn)“綁定”到 NSRunLoopDefaultMode 上的, 所以 Timer 是事情是不會(huì)被 RunLoop 處理的,我們的看到的時(shí)定時(shí)器被暫停了!
常見(jiàn)的解決方案是把Timer“綁定”到 NSRunLoopCommonModes 模式上, 那么Timer就可以與:
1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
這樣這個(gè)Timer就可以和當(dāng)前組中的兩種模式 UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 相關(guān)聯(lián)了。 RunLoop在這兩種模式下,Timer都可以正常運(yùn)行了。
注意: 由上面可以發(fā)現(xiàn) NSTimer 是不準(zhǔn)確的。 因?yàn)镽unLoop只負(fù)責(zé)分發(fā)源的消息。如果線程當(dāng)前正在處理繁重的任務(wù),比如循環(huán),就有可能導(dǎo)致Timer本次延時(shí),或者少執(zhí)行一次。網(wǎng)上有人做過(guò)實(shí)驗(yàn):
runloop_timer
上面的Log是一個(gè)間隔為 1 s 的計(jì)時(shí)器,我們可以發(fā)現(xiàn)在 12.836s ~ 15.835s 之間的時(shí)間段內(nèi), 明顯的 13s 的方法沒(méi)有執(zhí)行。 14s 的方法有所延遲。
因此當(dāng)我們用NSTimer來(lái)完成一些計(jì)時(shí)任務(wù)時(shí),如果需要比較精確的話,最好還是要比較“時(shí)間戳”。
3.6 常見(jiàn)問(wèn)題二:后臺(tái)的NSURLConnection不回調(diào),Timer不運(yùn)行
我們知道每個(gè)線程都有它的RunLoop, 我們可以通過(guò) [NSRunLoop currentRunLoop] 或 CFRunLoopGetCurrent() 來(lái)獲取。 但是主線程和后臺(tái)線程是不一樣的。主線程的RunLoop是一直在啟動(dòng)的。而后臺(tái)線程的RunLoop是默認(rèn)沒(méi)有啟動(dòng)的。
后臺(tái)線程的RunLoop沒(méi)有啟動(dòng)的情況下的現(xiàn)象就是:“代碼執(zhí)行完,線程就結(jié)束被回收了”。就像我們簡(jiǎn)單的程序執(zhí)行完就退出了。 所以如果我們希望在代碼執(zhí)行完成后還要保留線程等待一些異步的事件時(shí),比如NSURLConnection和NSTimer, 就需要手動(dòng)啟動(dòng)后臺(tái)線程的RunLoop。
啟動(dòng)RunLoop,我們需要設(shè)定RunLoop的模式,我們可以設(shè)置 NSDefaultRunLoopMode。 那默認(rèn)就是監(jiān)聽(tīng)所有時(shí)間源:
//Cocoa
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//Core Foundation
CFRunLoopRun();
我們也可以設(shè)置其他模式運(yùn)行, 甚至自定義運(yùn)行Mode,但是我們就需要把“事件源” “綁定”到該模式上:
extern NSString *kMyCustomRunLoopMode;
//NSURLConnection
[_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:kMyCustomRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:kMyCustomRunLoopMode beforeDate:[NSDate distantFuture]];
//Timer
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:kMyCustomRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:kMyCustomRunLoopMode beforeDate:[NSDate distantFuture]];
3.7 NSCommonRunLoopModes
這一節(jié)是2016-3-24補(bǔ)充
因?yàn)橐郧暗?.6的代碼有個(gè)錯(cuò)誤,而且對(duì)于NSRunLoopCommonModes沒(méi)有詳細(xì)說(shuō)明,造成有同學(xué)對(duì)這塊產(chǎn)生困惑, 因此有必要再開(kāi)一節(jié)補(bǔ)充下關(guān)于NSRunLoopCommonModes的概念。
NSRunLoopCommonModes 并不是一個(gè)真正的runLoopMode, 也就是說(shuō)這樣的寫法是錯(cuò)誤的:
1
[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; //wrong
這個(gè)寫法并不會(huì)讓runLoop運(yùn)行。
下面解釋下什么是CommonModes。
struct __CFRunLoop {
CFMutableSetRef _commonModes; // 有哪些Mode被標(biāo)記為Common
CFMutableSetRef _commonModeItems; // 這里面就是RunLoop的源,observer,Timer等
CFRunLoopModeRef _currentMode; // 當(dāng)前運(yùn)行的Mode
...
};
上面大概是CFRunLoop中有關(guān)CommonMode的結(jié)構(gòu)。這里面2個(gè)概念解釋一下:
在RunLoop里可以用CFRunLoopAddCommonMode將一個(gè)Mode標(biāo)記為Common屬性,那么這個(gè)Mode就會(huì)存在_commonModes里面。主線程默認(rèn)的kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 都已經(jīng)是CommonModes了,不需要再標(biāo)記。
_commonModeItems里面存放的源, observer, timer等,在每次runLoop運(yùn)行的時(shí)候都會(huì)被同步到具有Common標(biāo)記的Modes里。因此只要_currentMode是一個(gè)Common的Mode, 那么_commonModeItems里面的源,observer,timer也會(huì)執(zhí)行。
因此這樣addTimer時(shí),timer也會(huì)執(zhí)行的, 因?yàn)閠imer被添加到了_commonModeItems里面。
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] runMode:NSRunLoopDefaultMode beforeDate:[NSDate distantFuture]];
3.8 問(wèn)題三:本節(jié)開(kāi)頭的例子為何可以阻塞線程
while (!complete) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
你應(yīng)該知道這樣一段代碼可以阻塞當(dāng)前線程,你可能會(huì)奇怪:RunLoop就是不停循環(huán)來(lái)檢測(cè)源的事件,為什么還要加個(gè) while 呢?
這是因?yàn)镽unLoop的特性,RunLoop會(huì)在沒(méi)有“事件源”可監(jiān)聽(tīng)時(shí)休眠。也就是說(shuō)如果當(dāng)前沒(méi)有合適的“源”被RunLoop監(jiān)聽(tīng),那么這步就跳過(guò)了,不能起到阻塞線程的作用,所以還是要加個(gè)while循環(huán)來(lái)維持。
同時(shí)注意:因?yàn)檫@段代碼可以阻塞線程,所以請(qǐng)不要在主線程寫下這段代碼,因?yàn)樗芸赡軙?huì)導(dǎo)致界面卡住。
- 線程安全
講了這么多,你是否已經(jīng)對(duì)并發(fā)編程已經(jīng)躍躍欲試了呢? 但是并發(fā)編程一直都不是一個(gè)輕松的事情,使用并發(fā)編程會(huì)帶來(lái)許多陷阱。哪怕你是一個(gè)很成熟的程序員和架構(gòu)師,也很難避免線程安全的問(wèn)題;使用的越多,出錯(cuò)的可能就越大,因此可以不用多線程就不要使用。
關(guān)于并發(fā)編程的不可預(yù)見(jiàn)性有一個(gè)非常有名的例子:在1995年, NASA (美國(guó)宇航局)發(fā)送了開(kāi)拓者號(hào)火星探測(cè)器,但是當(dāng)探測(cè)器成功著陸在我們紅色的鄰居星球后不久,任務(wù)嘎然而止,火星探測(cè)器莫名其妙的不停重啟,在計(jì)算機(jī)領(lǐng)域內(nèi),遇到的這種現(xiàn)象被定為為優(yōu)先級(jí)反轉(zhuǎn),也就是說(shuō)低優(yōu)先級(jí)的線程一直阻塞著高優(yōu)先級(jí)的線程。在這里我們想說(shuō)明的是,即使擁有豐富的資源和大量?jī)?yōu)秀工程師的智慧,并發(fā)也還是會(huì)在不少情況下反咬你你一口。
4.1 資源共享和資源饑餓
并發(fā)編程中許多問(wèn)題的根源就是在多線程中訪問(wèn)共享資源。資源可以是一個(gè)屬性、一個(gè)對(duì)象,通用的內(nèi)存、網(wǎng)絡(luò)設(shè)備或者一個(gè)文件等等。在多線程中任何一個(gè)共享的資源都可能是一個(gè)潛在的沖突點(diǎn),你必須精心設(shè)計(jì)以防止這種沖突的發(fā)生。
一般我們通過(guò)鎖來(lái)解決資源共享的問(wèn)題,也就是可以通過(guò)對(duì)資源加鎖保證同時(shí)只有一個(gè)線程訪問(wèn)資源
4.1.1 互斥鎖
互斥訪問(wèn)的意思就是同一時(shí)刻,只允許一個(gè)線程訪問(wèn)某個(gè)特定資源。為了保證這一點(diǎn),每個(gè)希望訪問(wèn)共享資源的線程,首先需要獲得一個(gè)共享資源的互斥鎖。 對(duì)資源加鎖會(huì)引發(fā)一定的性能代價(jià)。
4.1.2 原子性
從語(yǔ)言層面來(lái)說(shuō),在 Objective-C 中將屬性以 atomic 的形式來(lái)聲明,就能支持互斥鎖了。事實(shí)上在默認(rèn)情況下,屬性就是 atomic 的。將一個(gè)屬性聲明為 atomic 表示每次訪問(wèn)該屬性都會(huì)進(jìn)行隱式的加鎖和解鎖操作。雖然最把穩(wěn)的做法就是將所有的屬性都聲明為 atomic,但是加解鎖這也會(huì)付出一定的代價(jià)。
4.1.3 死鎖
互斥鎖解決了競(jìng)態(tài)條件的問(wèn)題,但很不幸同時(shí)這也引入了一些其他問(wèn)題,其中一個(gè)就是死鎖。當(dāng)多個(gè)線程在相互等待著對(duì)方的結(jié)束時(shí),就會(huì)發(fā)生死鎖,這時(shí)程序可能會(huì)被卡住。
比如下面的代碼:
dispatch_sync(_queue, ^{
dispatch_sync(_queue, ^{
//do something
});
})
再比如:
main() {
dispatch_sync(dispatch_get_main_queue(), ^{
//do something
});
}
上面兩個(gè)例子也可以說(shuō)明 dispatch_sync 這個(gè)API是危險(xiǎn)的,所以盡量不要用。
當(dāng)你的代碼有死鎖的可能時(shí),它就會(huì)發(fā)生
4.1.4 資源饑餓
當(dāng)你認(rèn)為已經(jīng)足夠了解并發(fā)編程面臨的問(wèn)題時(shí),又出現(xiàn)了一個(gè)新的問(wèn)題。鎖定的共享資源會(huì)引起讀寫問(wèn)題。大多數(shù)情況下,限制資源一次只能有一個(gè)線程進(jìn)行讀取訪問(wèn)其實(shí)是非常浪費(fèi)的。因此,在資源上沒(méi)有寫入鎖的時(shí)候,持有一個(gè)讀取鎖是被允許的。這種情況下,如果一個(gè)持有讀取鎖的線程在等待獲取寫入鎖的時(shí)候,其他希望讀取資源的線程則因?yàn)闊o(wú)法獲得這個(gè)讀取鎖而導(dǎo)致資源饑餓的發(fā)生。
4.2 優(yōu)先級(jí)反轉(zhuǎn)
優(yōu)先級(jí)反轉(zhuǎn)是指程序在運(yùn)行時(shí)低優(yōu)先級(jí)的任務(wù)阻塞了高優(yōu)先級(jí)的任務(wù),有效的反轉(zhuǎn)了任務(wù)的優(yōu)先級(jí)。GCD提供了3種級(jí)別的優(yōu)先級(jí)隊(duì)列,分別是Default, High, Low。 高優(yōu)先級(jí)和低優(yōu)先級(jí)的任務(wù)之間共享資源時(shí),就可能發(fā)生優(yōu)先級(jí)反轉(zhuǎn)。當(dāng)?shù)蛢?yōu)先級(jí)的任務(wù)獲得了共享資源的鎖時(shí),該任務(wù)應(yīng)該迅速完成,并釋放掉鎖,這樣高優(yōu)先級(jí)的任務(wù)就可以在沒(méi)有明顯延時(shí)的情況下繼續(xù)執(zhí)行。然而高優(yōu)先級(jí)任務(wù)會(huì)在低優(yōu)先級(jí)的任務(wù)持有鎖的期間被阻塞。如果這時(shí)候有一個(gè)中優(yōu)先級(jí)的任務(wù)(該任務(wù)不需要那個(gè)共享資源),那么它就有可能會(huì)搶占低優(yōu)先級(jí)任務(wù)而被執(zhí)行,因?yàn)榇藭r(shí)高優(yōu)先級(jí)任務(wù)是被阻塞的,所以中優(yōu)先級(jí)任務(wù)是目前所有可運(yùn)行任務(wù)中優(yōu)先級(jí)最高的。此時(shí),中優(yōu)先級(jí)任務(wù)就會(huì)阻塞著低優(yōu)先級(jí)任務(wù),導(dǎo)致低優(yōu)先級(jí)任務(wù)不能釋放掉鎖,這也就會(huì)引起高優(yōu)先級(jí)任務(wù)一直在等待鎖的釋放。如下圖:
使用不同優(yōu)先級(jí)的多個(gè)隊(duì)列聽(tīng)起來(lái)雖然不錯(cuò),但畢竟是紙上談兵。它將讓本來(lái)就復(fù)雜的并行編程變得更加復(fù)雜和不可預(yù)見(jiàn)。因此我們寫代碼的時(shí)候最好只用Default優(yōu)先級(jí)的隊(duì)列,不要使用其他隊(duì)列來(lái)讓問(wèn)題復(fù)雜化。
關(guān)于dispatch_queue的底層線程安全設(shè)計(jì)可參考:底層并發(fā) API
- 總結(jié)
本文主要講了 NSOperationQueue、 NSRunLoop、 和線程安全等三大塊內(nèi)容。 希望可以幫助你理解 NSOperation的使用, NSRunLoop的作用, 還有并發(fā)編程帶來(lái)的復(fù)雜性和相關(guān)問(wèn)題。
并發(fā)實(shí)際上是一個(gè)非常棒的工具。它充分利用了現(xiàn)代多核 CPU 的強(qiáng)大計(jì)算能力。但是因?yàn)樗膹?fù)雜性,所以我們盡量使用高級(jí)的API,盡量寫簡(jiǎn)單的代碼,讓并發(fā)模型保持簡(jiǎn)單; 這樣可以寫出高效、結(jié)構(gòu)清晰、且安全的代碼。
參考和引文
1、https://objccn.io/issue-2-1/