iOS之多線程相關問題

一、進程、線程及關系

一、什么是進程?

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

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

  • 每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內,擁有獨立運行所需的全部資源。

二、什么是線程?

  • 線程是CPU調度的最小單位。

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

三、進程和線程的關系

進程和線程的關系,可以簡單的形象的做個比喻:進程=火車,線程=車廂

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

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

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

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

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

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

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

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

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

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

一、任務

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

二、隊列

一、隊列有兩種:

  1. 串行隊列:

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

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

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

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

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

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

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

隊列和任務的關系

任務和隊列的關系可以打一個形象的比喻,比如小時后玩的四驅車,隊列=跑道,串行隊列=一條跑道,并行隊列=多條跑道,四驅車=任務)。

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

  • 執(zhí)行隊列的方式就相當于我們如何把車(任務)放到跑道(隊列)里

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

    • 就相當于我們只有主線程一只手,每次只能把一輛車(任務)放到跑道上跑,等車跑完,把車收了以后,才能把下一輛車(任務)放到跑道上跑

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

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

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

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

四、多線程引起的死鎖

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

產生死鎖的情況:

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

在主線程中運用主隊列同步,也就是把任務放到了主線程的隊列中。

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

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

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

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

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

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

這是因為里面的任務和外面的任務都在同一個serialQueue隊列內,又是同步,這就和上邊主隊列同步的例子一樣造成了死鎖

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

二、在不同的串行隊列,異步block調用同步不會引起死鎖

 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是在同一個線程中的。

打印結果:

serialQueue2
serialQueue

五、GCD任務執(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");

代碼打印結果為:1 3 2 4 5

首先先打印1,

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

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

又因為任務4是同步任務,會阻塞線程,所以再打印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)建提交任務到runloop上的。

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

解決方法:

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

  2. 或者將dispatch_async改成同步,因為同步是在當前線程執(zhí)行,那么如果當前線程是主線程,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)不同之處在于當自旋鎖嘗試獲取鎖時以忙等待(busy waiting)的形式不斷地循環(huán)檢查鎖是否可用。

  • 當上一個線程的任務沒有執(zhí)行完畢的時候(被鎖住),那么下一個線程會一直等待(不會睡眠),當上一個線程的任務執(zhí)行完畢,下一個線程會立即執(zhí)行。

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

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

自旋鎖會忙等: 所謂忙等,即在訪問被鎖資源時,調用者線程不會休眠,而是不停循環(huán)在那里,直到被鎖資源釋放鎖。

二、互斥鎖

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

  • 當上一個線程的任務執(zhí)行完畢,下一個線程會自動喚醒然后執(zhí)行任務。

互斥鎖會休眠: 所謂休眠,即在訪問被鎖資源時,調用者線程會休眠,此時cpu可以調度其他線程工作。直到被鎖資源釋放鎖。此時會喚醒休眠線程。

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

自旋鎖優(yōu)點:

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

  • 所有如果能在很短的時間內獲得鎖,自旋鎖的效率遠高于互斥鎖。

自旋鎖缺點:

  • 自旋鎖一直占用CPU,他在未獲得鎖的情況下,一直運行--自旋,所以占用著CPU,如果不能在很短的時 間內獲得鎖,這無疑會使CPU效率降低。
  • 自旋鎖不能實現(xiàn)遞歸調用。

自旋鎖:

  • atomic
  • OSSpinLock
  • dispatch_semaphore_t 等

互斥鎖如:

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

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

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

  • 由于每次開辟子線程都會消耗cpu,在需要頻繁使用子線程的情況下,頻繁開辟子線程會消耗大量的cpu,而且創(chuàng)建線程都是任務執(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í)行結果并沒有打印,說明test方法沒有被調用。

當創(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直接調用

[self performSelector: onThread: withObject: waitUntilDone:]
 

三、常駐線程退出

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

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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容