多線程陷阱

前言

隨著手機(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í)行完成的书闸,這種資源搶占被稱作優(yōu)先級(jí)反轉(zhuǎn)設(shè)計(jì)良好的多線程方案會(huì)優(yōu)先執(zhí)行高優(yōu)先級(jí)的任務(wù)利凑,在遇到多種優(yōu)先級(jí)任務(wù)處理的時(shí)候浆劲,可能會(huì)發(fā)生優(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)建传黄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市队寇,隨后出現(xiàn)的幾起案子膘掰,更是在濱河造成了極大的恐慌,老刑警劉巖佳遣,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件识埋,死亡現(xiàn)場(chǎng)離奇詭異凡伊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)窒舟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門系忙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人辜纲,你說我怎么就攤上這事笨觅。” “怎么了耕腾?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)杀糯。 經(jīng)常有香客問我扫俺,道長(zhǎng),這世上最難降的妖魔是什么固翰? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任狼纬,我火速辦了婚禮,結(jié)果婚禮上骂际,老公的妹妹穿的比我還像新娘疗琉。我一直安慰自己,他們只是感情好歉铝,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布盈简。 她就那樣靜靜地躺著,像睡著了一般太示。 火紅的嫁衣襯著肌膚如雪柠贤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天类缤,我揣著相機(jī)與錄音臼勉,去河邊找鬼。 笑死餐弱,一個(gè)胖子當(dāng)著我的面吹牛宴霸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膏蚓,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼瓢谢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了降允?” 一聲冷哼從身側(cè)響起恩闻,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎剧董,沒想到半個(gè)月后幢尚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體破停,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年尉剩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了真慢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡理茎,死狀恐怖黑界,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情皂林,我是刑警寧澤朗鸠,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站础倍,受9級(jí)特大地震影響烛占,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沟启,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一忆家、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧德迹,春花似錦芽卿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至流酬,卻和暖如春币厕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芽腾。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工旦装, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摊滔。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓阴绢,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親艰躺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呻袭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容