前言
隨著手機(jī)硬件的升級(jí)滋迈,多線程技術(shù)在應(yīng)用開發(fā)中的地位可以說足以媲美UITableView
了材义。然而颖对,多線程技術(shù)在提供我們生產(chǎn)力的同時(shí)赏寇,也不可避免的帶來(lái)了陷阱吉嫩,正如著名計(jì)算機(jī)學(xué)者所言:能力越大,bug越大
本文嘗試從多個(gè)角度聊聊這些陷阱嗅定。
內(nèi)存占用
線程的創(chuàng)建需要占用一定的內(nèi)核物理內(nèi)存以及CPU
處理時(shí)間自娩,具體消耗參見下表。
類型 | 消耗估算 | 詳情 |
---|---|---|
內(nèi)核結(jié)構(gòu)體 | 1KB | 存儲(chǔ)線程數(shù)據(jù)結(jié)構(gòu)和屬性 |
椙耍空間 | 子線程(512KB) Mac主線程(8MB) iOS主線程(1MB) |
堆棧大小必須為4KB的倍數(shù) 子線程的最小內(nèi)存為16KB |
創(chuàng)建時(shí)間 | 90微秒 | 1G內(nèi)存 Intel 2GHz CPU Mac OS X v10.5 |
此外在CPU
上切換線程上下文的花銷也是不廉價(jià)的忙迁,這些花銷體現(xiàn)在切換線程上下文時(shí)更新寄存器、尋址搜索等碎乃。這兩種花銷在并發(fā)編程時(shí)姊扔,可能會(huì)出現(xiàn)非常明顯的性能下降。
共享資源
對(duì)于使用共享資源的陷阱主要發(fā)生在兩點(diǎn):線程競(jìng)爭(zhēng)以及鎖
-
線程競(jìng)爭(zhēng)
多個(gè)線程同時(shí)對(duì)共有的資源進(jìn)行寫操作時(shí)梅誓,會(huì)產(chǎn)生數(shù)據(jù)錯(cuò)誤恰梢,這種錯(cuò)誤難以被發(fā)現(xiàn),可能會(huì)導(dǎo)致應(yīng)用無(wú)法繼續(xù)正常運(yùn)行梗掰。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int idx = 0; idx < 100; idx++) {
_flag--;
}
});dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ for (int idx = 0; idx < 100; idx++) { _flag++; } });
鎖的開銷
鎖是為了解決線程競(jìng)爭(zhēng)錯(cuò)誤設(shè)計(jì)的方案嵌言,提供了不同的方式保證多個(gè)線程對(duì)共享資源的訪問限制。iOS
提供了多種線程鎖供我們使用及穗,具體的鎖在這里就不再闡述摧茴。鎖的操作不當(dāng)會(huì)導(dǎo)致死鎖出現(xiàn),從而使得整個(gè)線程無(wú)法繼續(xù)執(zhí)行埂陆。
- (int)recursiveToCalculateSum: (int)number {
[_lock lock];
_sum += (number <= 1 ? 1 : [self recursiveToCalculateSum: number - 1]);
[_lock unlock];
}
線程死鎖
線程死鎖與鎖的死鎖是兩個(gè)概念蓬蝶,但其原因其實(shí)是一樣的尘分。當(dāng)我們同步派發(fā)任務(wù)到當(dāng)前隊(duì)列執(zhí)行的時(shí)候猜惋。隊(duì)列堵塞丸氛,等待派發(fā)任務(wù)的執(zhí)行。但由于隊(duì)列堵塞著摔,派發(fā)任務(wù)永遠(yuǎn)無(wú)法執(zhí)行缓窜,形成一個(gè)死循環(huán)。通過libdispatch的源碼我們可以發(fā)現(xiàn)實(shí)際上sync
內(nèi)部是個(gè)信號(hào)加鎖操作谍咆,且sync
對(duì)于global_queue
和自定義隊(duì)列來(lái)說是直接執(zhí)行禾锤,不會(huì)將任務(wù)壓入棧中。其代碼可以表示為:
do_task_in_target_queue(target, ^{
shared = SEM_GET_SHARED(sem);
sem_wait(shared);
task();
sem_post(shared);
});
事實(shí)上sync
操作是個(gè)無(wú)限等待的加鎖操作摹察,所以當(dāng)sync
到當(dāng)前線程的時(shí)候引發(fā)的是死鎖問題恩掷。這也是為什么線程死鎖實(shí)際上并非同步隊(duì)列的問題,只是一個(gè)簡(jiǎn)單的死鎖供嚎。
線程被颇铮活
線程的釋放是個(gè)不容易被注重到的細(xì)節(jié),我們都知道NSTimer
的準(zhǔn)確度在很多時(shí)候不盡人意克滴,為了提高精確度逼争,很多人會(huì)在子線程啟動(dòng)RunLoop
保活(全局線程不存在釋放上的問題)劝赔。比如著名的AFNetworking
啟用了一個(gè)空的NSPort
端口保證回調(diào)線程笔慕梗活:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
在蘋果官方文檔中,啟動(dòng)RunLoop
的有三種方式:
- [NSRunLoop run];
- [NSRunLoop runUntilDate: [NSDate date]];
- [NSRunLoop runMode: NSRUnloopDefaultModes beforeDate: [NSDate date]];
除了后面兩者之外着帽,第一種方式必須調(diào)用kill
的方式殺死它才能結(jié)束杂伟,這也是不當(dāng)使用RunLoop
的陷阱之一。采用CFRunLoopRef
的相關(guān)方法完成啟動(dòng)和停止是一種更好的做法仍翰。
CFRunLoopRun();
CFRunLoopStop(CFRunLoopGetCurrent());
隊(duì)列優(yōu)先級(jí)
更高優(yōu)先級(jí)的任務(wù)在能更好的搶占CPU
資源赫粥,這導(dǎo)致了低優(yōu)先級(jí)方案在處理任務(wù)加鎖時(shí)可能導(dǎo)致被搶占執(zhí)行,從而導(dǎo)致鎖無(wú)法正常打開歉备,導(dǎo)致另一種特殊的死鎖傅是。在不再安全的OSSpinLock中就提到了這一點(diǎn)。在GCD
中系統(tǒng)創(chuàng)建了四種常駐并行隊(duì)列蕾羊,分別對(duì)應(yīng)不同優(yōu)先級(jí)的任務(wù)處理:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
如果按照從低到高
的順序向這四個(gè)隊(duì)列里面派發(fā)大量的日志輸出任務(wù)喧笔,可以看到在運(yùn)行沒有多久的時(shí)間后,DISPATCH_QUEUE_PRIORITY_HIGH
的任務(wù)會(huì)比提前于調(diào)用次序運(yùn)行龟再,而DISPATCH_QUEUE_PRIORITY_BACKGROUND
總是接近最后執(zhí)行完成的书闸,這種資源搶占被稱作設(shè)計(jì)良好的多線程方案會(huì)優(yōu)先執(zhí)行高優(yōu)先級(jí)的任務(wù)利凑,在遇到多種優(yōu)先級(jí)任務(wù)處理的時(shí)候浆劲,可能會(huì)發(fā)生優(yōu)先級(jí)反轉(zhuǎn)
。優(yōu)先級(jí)反轉(zhuǎn)
(詳見評(píng)論處)嫌术,這時(shí)候OSSpinLock
自旋鎖會(huì)因?yàn)檫@種反轉(zhuǎn)變得不安全。
另一個(gè)問題是Custom Queue
的線程優(yōu)先級(jí)總是為DISPATCH_QUEUE_PRIORITY_DEFAULT
牌借,這意味著在某些時(shí)刻可能我們?cè)趧?chuàng)建的串行隊(duì)列上執(zhí)行的任務(wù)也不一定是安全的度气。iOS8
之后為自定義線程提供了QualityOfServer
用來(lái)標(biāo)志線程優(yōu)先級(jí)。
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceDefault = -1
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
}
LXD_INLINE dispatch_queue_attr_t __LXDQoSToQueueAttributes(LXDQualityOfService qos) {
dispatch_qos_class_t qosClass = __LXDQualityOfServiceToQOSClass(qos);
return dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, qosClass, 0);
};
這意味著開發(fā)者對(duì)于多線程通過使用搭配不同優(yōu)先級(jí)的自定義串行隊(duì)列來(lái)更靈活的完成任務(wù)膨报。
并發(fā)噩夢(mèng)
系統(tǒng)本身提供了四種優(yōu)先級(jí)的并行隊(duì)列
給開發(fā)者使用磷籍,這意味著當(dāng)我們async
任務(wù)到這些全局線程中執(zhí)行的時(shí)候,為了充分的發(fā)揮CPU
的執(zhí)行效率现柠,GCD
可能會(huì)多次創(chuàng)建線程來(lái)執(zhí)行新的任務(wù)院领。
方便意味著隱藏的代價(jià)。試想一下這個(gè)場(chǎng)景够吩,當(dāng)前CPU
核心正在執(zhí)行一個(gè)IO
操作比然,然后進(jìn)入等待磁盤響應(yīng)的狀態(tài)。在這個(gè)時(shí)間點(diǎn)上周循,CPU
核心是處在未利用的狀態(tài)下的强法。這時(shí)候GCD
一看:丫的偷懶?然后創(chuàng)建一個(gè)新的線程執(zhí)行任務(wù)鱼鼓。假如派發(fā)的任務(wù)總是耗時(shí)的拟烫,且需要等待響應(yīng)。那么GCD
會(huì)不斷的創(chuàng)建新的線程來(lái)充分利用CPU
迄本。當(dāng)線程創(chuàng)建的足夠多的時(shí)候硕淑,GCD
會(huì)嘗試釋放線程來(lái)減少壓力。但是由于線程中的IO
操作并沒有執(zhí)行完成嘉赎,因此導(dǎo)致大量的線程無(wú)法釋放置媳,占據(jù)了大量的內(nèi)存使用。
for (NSInteger idx = 0; idx < N; idx++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString * filePath = [self filePathWithFileName: fileName];
NSData * data = [NSData dataWithContentsOfFile: filePath];
/// do something
});
}
一旦這時(shí)候磁盤響應(yīng)公条,開始讀取數(shù)據(jù)拇囊,這些線程爭(zhēng)奪CPU
資源,占用的內(nèi)存足以讓開發(fā)者崩潰靶橱。解決方案之一是我在GCD封裝中封裝的串行隊(duì)列
執(zhí)行方案寥袭,采用QoS
對(duì)線程進(jìn)行優(yōu)先級(jí)設(shè)定,保證緊急任務(wù)優(yōu)先得到處理关霸。此外根據(jù)CPU
核心創(chuàng)建的等量串行可以保證CPU
核心得到最大利用化以及避免了并發(fā)隊(duì)列過度的線程創(chuàng)建传黄。