iOS之多線程相關(guān)問題

一蕊梧、進程蹦渣、線程及關(guān)系

一、什么是進程揣炕?

  • 進程是具有一定獨立功能程序關(guān)于某次數(shù)據(jù)集合的一次運行活動秕狰,操作系統(tǒng)分配資源的基本單元也是最小單位稠腊。

  • 進程是指在系統(tǒng)中正在運行的一個應(yīng)用程序,就是一段程序的執(zhí)行過程,我們可以理解為手機上的一個app就是一個進程鸣哀。

  • 每個進程之間是獨立的麻养,每個進程均運行在其專用且受保護的內(nèi)存空間內(nèi),擁有獨立運行所需的全部資源诺舔。

二、什么是線程备畦?

  • 線程是CPU調(diào)度的最小單位低飒。

  • 一個進程要想執(zhí)行任務(wù),必須至少有一條線程.應(yīng)用程序啟動的時候,系統(tǒng)會默認開啟一條線程,也就是主線程

三懂盐、進程和線程的關(guān)系

進程和線程的關(guān)系褥赊,可以簡單的形象的做個比喻:進程=火車,線程=車廂

  • 線程須在進程下才能運行的(單純的車廂無法運行)

  • 一個進程可以包含多個線程(一輛火車可以有多個車廂)

  • 不同進程間的數(shù)據(jù)很難共享(一輛火車上的乘客很難換到另外一輛火車莉恼,比如站點換乘)

  • 同一進程下不同線程間數(shù)據(jù)很易共享(A車廂換到B車廂很容易)

  • 進程要比線程消耗更多的計算機資源(采用多列火車相比多個車廂更耗資源)

  • 進程間不會相互影響拌喉,一個線程掛掉將導(dǎo)致整個進程掛掉(一列火車不會影響到另外一列火車速那,但是如果一列火車中一節(jié)車廂著火了,將影響到所有車廂)

  • 進程可以拓展到多機尿背,線程最多適合多核不同火車(可以開在多個軌道上端仰,同一火車的車廂不能在行進的不同的軌道上)

  • 進程使用的內(nèi)存地址可以上鎖,即一個線程使用某些共享內(nèi)存時田藐,其他線程必須等它結(jié)束荔烧,才能使用這一塊內(nèi)存。(比如火車上的洗手間)-"互斥鎖"

  • 進程使用的內(nèi)存地址可以限定使用量(比如火車上的餐廳汽久,最多只允許多少人進入鹤竭,如果滿了需要在門口等,等有人出來了才能進去)-“信號量”

二景醇、任務(wù)臀稚、隊列及執(zhí)行任務(wù)的方式

一、任務(wù)

  • 任務(wù)就是要執(zhí)行的操作三痰,也就是你在線程中執(zhí)行的那段代碼吧寺。在 GCD 中是放在 block 中的。

二酒觅、隊列

一撮执、隊列有兩種:

  1. 串行隊列:

    • 每次只有一個任務(wù)被執(zhí)行。讓任務(wù)一個接著一個地執(zhí)行舷丹。(只開啟一個線程抒钱,一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù))
  2. 并行隊列:

    • 可以讓多個任務(wù)并發(fā)(同時)執(zhí)行颜凯。(可以開啟多個線程谋币,并且同時執(zhí)行任務(wù))

二、執(zhí)行隊列的方式

  1. 同步執(zhí)行(sync):

    • 同步添加任務(wù)到指定的隊列中症概,在添加的任務(wù)執(zhí)行結(jié)束之前蕾额,會一直等待,直到隊列里面的任務(wù)完成之后再繼續(xù)執(zhí)行彼城。
    • 只能在當(dāng)前線程中執(zhí)行任務(wù)诅蝶,不具備開啟新線程的能力。
  2. 異步執(zhí)行(async):

  • 異步添加任務(wù)到指定的隊列中募壕,它不會造成主線程的等待调炬,可以繼續(xù)執(zhí)行任務(wù).

  • 可以在新的線程中執(zhí)行任務(wù),具備開啟新線程的能力舱馅。

隊列和任務(wù)的關(guān)系

任務(wù)和隊列的關(guān)系可以打一個形象的比喻缰泡,比如小時后玩的四驅(qū)車,隊列=跑道代嗤,串行隊列=一條跑道棘钞,并行隊列=多條跑道缠借,四驅(qū)車=任務(wù))。

基本規(guī)則就是每條跑道上只能有一輛車在上面跑宜猜。串行隊列由于只有一條跑道泼返,所以每次只能跑一輛車(一個任務(wù)),等這輛車跑完宝恶,別的車(任務(wù))才能跑符隙。并發(fā)隊列由于有多個跑道,所以可以供多輛車(多個任務(wù))一起跑垫毙。

  • 執(zhí)行隊列的方式就相當(dāng)于我們?nèi)绾伟衍嚕ㄈ蝿?wù))放到跑道(隊列)里

  • 同步執(zhí)行不開啟新線程:

    • 就相當(dāng)于我們只有主線程一只手霹疫,每次只能把一輛車(任務(wù))放到跑道上跑,等車跑完综芥,把車收了以后丽蝎,才能把下一輛車(任務(wù))放到跑道上跑

    • 所以不管我們是把車(任務(wù))放到單條跑道(串行隊列)還是把車(任務(wù))放到多條跑道(并發(fā)隊列),每次都只能控制一輛車膀藐。并且由于主線程在玩車(執(zhí)行任務(wù))屠阻,也就干不了別的事,所以同步執(zhí)行會造成主線程等待额各。

  • 異步執(zhí)行可以開啟新線程:

    • 就相當(dāng)于我們除了有主線程這只手外還邀請了很多一起玩的小伙伴(分線程)国觉,我們每個人都能拿起一輛車(任務(wù))放到對應(yīng)的跑道(隊列)上,然后一起跑虾啦。所以在多條跑道(并發(fā)隊列)上多輛車(多個任務(wù))可以一起跑麻诀。

    • 如果車(任務(wù))很多,跑道只有一個(串行隊列)傲醉,那么還是得排隊玩蝇闭,每次只能跑一輛車(任務(wù))。但是硬毕,由于邀請了小伙伴(開啟了線程)呻引,主線程這只手就可以讓小伙伴(分線程)先玩車,自己去處理別的事情吐咳。所以異步執(zhí)行不會造成主線程等待逻悠。

四、多線程引起的死鎖

死鎖就是隊列引起的循環(huán)等待韭脊。

產(chǎn)生死鎖的情況:

  • 一個比較常見的死鎖例子:主隊列同步
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_sync(dispatch_get_main_queue(), ^{
       
        NSLog(@"deallock");
    });
}

在主線程中運用主隊列同步蹂风,也就是把任務(wù)放到了主線程的隊列中。

同步對于任務(wù)是立刻執(zhí)行的乾蓬,那么當(dāng)把任務(wù)放進主隊列時,它就會立馬執(zhí)行,只有執(zhí)行完這個任務(wù)慎恒,viewDidLoad才會繼續(xù)向下執(zhí)行任内。

viewDidLoad和任務(wù)都是在主隊列上的撵渡,由于隊列的先進先出原則,任務(wù)又需等待viewDidLoad執(zhí)行完畢后才能繼續(xù)執(zhí)行死嗦,viewDidLoad和這個任務(wù)就形成了相互循環(huán)等待趋距,就造成了死鎖。

想避免這種死鎖越除,可以將同步改成異步dispatch_async,或者將dispatch_get_main_queue換成其他串行或并行隊列节腐,都可以解決。

  • 二摘盆、在同串行隊列下異步任務(wù)里調(diào)用同步任務(wù)
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
       
    dispatch_sync(serialQueue, ^{
            
        NSLog(@"deadlock");
    });
});

外面的函數(shù)無論是同步還是異步都會造成死鎖翼雀。

這是因為里面的任務(wù)和外面的任務(wù)都在同一個serialQueue隊列內(nèi),又是同步孩擂,這就和上邊主隊列同步的例子一樣造成了死鎖

解決方法也和上邊一樣狼渊,將里面的同步改成異步dispatch_async,或者將serialQueue換成其他串行或并行隊列,都可以解決

二类垦、在不同的串行隊列狈邑,異步block調(diào)用同步不會引起死鎖

 dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
dispatch_async(serialQueue, ^{
        
  dispatch_sync(serialQueue2, ^{
            
    NSLog(@"serialQueue2");
    });
    NSLog(@"serialQueue");
});

serialQueue、serialQueue2是兩個不同隊列蚤认,所以不存在隊列引起的循環(huán)等待米苹。

這樣是不會死鎖的,并且serialQueue和serialQueue2是在同一個線程中的。

打印結(jié)果:

serialQueue2
serialQueue

五砰琢、GCD任務(wù)執(zhí)行順序

  • 1蘸嘶、串行隊列先異步后同步
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
NSLog(@"1");
    
dispatch_async(serialQueue, ^{
        
    NSLog(@"2");
});
    
    NSLog(@"3");
    
dispatch_sync(serialQueue, ^{
        
    NSLog(@"4");
});

NSLog(@"5");

代碼打印結(jié)果為:1 3 2 4 5

首先先打印1,

接下來將任務(wù)2其添加至串行隊列上,由于任務(wù)2是異步氯析,不會阻塞線程亏较,繼續(xù)向下執(zhí)行,打印3

然后是任務(wù)4掩缓,將任務(wù)4添加到串行隊列上雪情,因為任務(wù)4和任務(wù)2在同一串行隊列,根據(jù)隊列先進先出原則你辣,任務(wù)4必須等任務(wù)2執(zhí)行后才能執(zhí)行巡通。所以先打印2

又因為任務(wù)4是同步任務(wù),會阻塞線程舍哄,所以再打印4.

最后打印5

  • 2宴凉、performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        [self performSelector:@selector(test:) withObject:nil afterDelay:0];
    });

這里的test方法是不會去執(zhí)行的,原因在于

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument 
afterDelay:(NSTimeInterval)delay;

這個方法要創(chuàng)建提交任務(wù)到runloop上的表悬。

gcd底層創(chuàng)建的子線程是默認沒有開啟對應(yīng)runloop的弥锄,所有這個方法就會失效。

解決方法:

  1. 將dispatch_get_global_queue改成主隊列,由于主隊列所在的主線程是默認開啟了runloop的籽暇,就會去執(zhí)行温治。

  2. 或者將dispatch_async改成同步,因為同步是在當(dāng)前線程執(zhí)行戒悠,那么如果當(dāng)前線程是主線程熬荆,test方法也是會去執(zhí)行的,如果不是就還是不執(zhí)行)绸狐。

  3. 在就是開啟子線程的run卤恳,在perfomselector下開發(fā)runloop就行代碼如下:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        [self performSelector:@selector(test:) withObject:nil afterDelay:0];
        [[NSRunLoop currentRunLoop] run];
    });

開啟子線程的runloop就會執(zhí)行了。

六寒矿、自旋鎖與互斥鎖

一突琳、自旋鎖

  • 自旋鎖是一種用于保護多線程共享資源的鎖,與一般互斥鎖(mutex)不同之處在于當(dāng)自旋鎖嘗試獲取鎖時以忙等待(busy waiting)的形式不斷地循環(huán)檢查鎖是否可用劫窒。

  • 當(dāng)上一個線程的任務(wù)沒有執(zhí)行完畢的時候(被鎖妆窘瘛),那么下一個線程會一直等待(不會睡眠)主巍,當(dāng)上一個線程的任務(wù)執(zhí)行完畢冠息,下一個線程會立即執(zhí)行。

使用場景:對持有鎖較短的程序

在多CPU的環(huán)境中孕索,對持有鎖較短的程序來說逛艰,使用自旋鎖代替一般的互斥鎖往往能夠提高程序的性能。

自旋鎖會忙等: 所謂忙等搞旭,即在訪問被鎖資源時散怖,調(diào)用者線程不會休眠,而是不停循環(huán)在那里肄渗,直到被鎖資源釋放鎖镇眷。

二、互斥鎖

  • 互斥鎖:當(dāng)上一個線程的任務(wù)沒有執(zhí)行完畢的時候(被鎖佐岬铡)欠动,那么下一個線程會進入睡眠狀態(tài)等待任務(wù)執(zhí)行完畢。

  • 當(dāng)上一個線程的任務(wù)執(zhí)行完畢惑申,下一個線程會自動喚醒然后執(zhí)行任務(wù)具伍。

互斥鎖會休眠: 所謂休眠,即在訪問被鎖資源時圈驼,調(diào)用者線程會休眠人芽,此時cpu可以調(diào)度其他線程工作。直到被鎖資源釋放鎖绩脆。此時會喚醒休眠線程萤厅。

三橄抹、各自優(yōu)缺點:

自旋鎖優(yōu)點:

  • 自旋鎖的優(yōu)點在于,因為自旋鎖不會引起調(diào)用者睡眠祈坠,所以不會進行線程調(diào)度害碾,CPU時間片輪轉(zhuǎn)等耗時操作。

  • 所有如果能在很短的時間內(nèi)獲得鎖赦拘,自旋鎖的效率遠高于互斥鎖。

自旋鎖缺點:

  • 自旋鎖一直占用CPU芬沉,他在未獲得鎖的情況下躺同,一直運行--自旋,所以占用著CPU丸逸,如果不能在很短的時 間內(nèi)獲得鎖蹋艺,這無疑會使CPU效率降低。
  • 自旋鎖不能實現(xiàn)遞歸調(diào)用黄刚。

自旋鎖:

  • atomic
  • OSSpinLock
  • dispatch_semaphore_t 等

互斥鎖如:

  • pthread_mutex
  • @ synchronized
  • NSLock捎谨、NSConditionLock
  • NSCondition
  • NSRecursiveLock等

六、NSThread+runloop實現(xiàn)常駐線程

一憔维、為什么去實現(xiàn)常駐線程涛救,常駐線程使用背景?

  • 由于每次開辟子線程都會消耗cpu业扒,在需要頻繁使用子線程的情況下检吆,頻繁開辟子線程會消耗大量的cpu,而且創(chuàng)建線程都是任務(wù)執(zhí)行完成之后也就釋放了程储,不能再次利用蹭沛。

  • 那么如何創(chuàng)建一個線程可以讓它可以再次工作呢?也就是創(chuàng)建一個常駐線程章鲤。

一摊灭、實現(xiàn)常駐線程的步驟

  1. 首先常駐線程既然是常駐,那么可以用GCD實現(xiàn)一個單例來保存NSThread 代碼如下:
+ (NSThread *)shareThread {
    
    static NSThread *_shareThread = nil;
    
    static dispatch_once_t oncePredicate;
    
    dispatch_once(&oncePredicate, ^{
        
        _shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];

        [_shareThread setName:@"threadTest"];
        
        [_shareThread start];
    });
    
    return _shareThread;
}

這樣創(chuàng)建的thread就不會銷毀了嗎败徊?

[self performSelector:@selector(test) onThread:[ViewController shareThread] withObject:nil waitUntilDone:NO];

- (void)test {
    NSLog(@"test:%@", [NSThread currentThread]);
}

執(zhí)行結(jié)果并沒有打印帚呼,說明test方法沒有被調(diào)用。

當(dāng)創(chuàng)建的NSThread沒有開啟runloop的話集嵌,常駐線是不會有效的萝挤。

因為新建的子線程默認沒有開啟runloop

因此需要給這個線程添加了一個runloop,并且加了一個NSMachPort端口監(jiān)聽根欧,防止新建的線程由于沒有活動直接退出怜珍。

  1. 其次給線程開啟runloop。

需要給這個線程添加了一個runloop凤粗,并且加了一個NSMachPort端口監(jiān)聽酥泛,防止新建的線程由于沒有活動直接退出今豆。。

- (void)threadTest
{
    @autoreleasepool {
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        
        [runLoop run];
    }
}

二柔袁、常駐線程的使用

performSelector是封裝在NSObject的NSThreadPerformAdditions類別里呆躲,只要是NSObject直接調(diào)用

[self performSelector: onThread: withObject: waitUntilDone:]
 

三、常駐線程退出

只有從runloop中移除我們之前添加的端口捶索,這樣runloop沒有任何事件插掂,所以直接退出。方法如下:

[NSRunLoop currentRunLoop]removePort:<#(nonnull NSPort *)#> forMode:<#(nonnull NSRunLoopMode)#>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腥例,一起剝皮案震驚了整個濱河市辅甥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌燎竖,老刑警劉巖璃弄,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異构回,居然都是意外死亡夏块,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門纤掸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脐供,“玉大人,你說我怎么就攤上這事茁肠』济瘢” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵垦梆,是天一觀的道長匹颤。 經(jīng)常有香客問我,道長托猩,這世上最難降的妖魔是什么印蓖? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮京腥,結(jié)果婚禮上赦肃,老公的妹妹穿的比我還像新娘。我一直安慰自己公浪,他們只是感情好他宛,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著欠气,像睡著了一般厅各。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上预柒,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天队塘,我揣著相機與錄音袁梗,去河邊找鬼。 笑死憔古,一個胖子當(dāng)著我的面吹牛遮怜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸿市,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼锯梁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了焰情?” 一聲冷哼從身側(cè)響起涝桅,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎烙样,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蕊肥,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡谒获,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了壁却。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片批狱。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖展东,靈堂內(nèi)的尸體忽然破棺而出赔硫,到底是詐尸還是另有隱情,我是刑警寧澤盐肃,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布爪膊,位于F島的核電站,受9級特大地震影響砸王,放射性物質(zhì)發(fā)生泄漏推盛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一谦铃、第九天 我趴在偏房一處隱蔽的房頂上張望耘成。 院中可真熱鬧,春花似錦驹闰、人聲如沸瘪菌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽师妙。三九已至,卻和暖如春骡显,著一層夾襖步出監(jiān)牢的瞬間疆栏,已是汗流浹背曾掂。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留壁顶,地道東北人珠洗。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像若专,于是被迫代替她去往敵國和親许蓖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

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