iOS源碼解析:多線程<一>

iOS開發(fā)中經(jīng)常要使用到多線程,在面試的時候也是經(jīng)常問到,比較常見的面試題有下面這些:

  • iOS的多線程方案有哪幾種?你更傾向于哪一種?
  • GCD的隊列類型。
  • 說一下OperationQueue和GCD的區(qū)別,以及各自的優(yōu)勢。
  • 線程安全的處理手段有哪些?
  • OC你了解的鎖有哪些?在此基礎(chǔ)上進行二次提問“
    1.自旋和互斥對比
    2.使用以上鎖需要注意哪些?
    3.用C/OC/C++,任選其一,實現(xiàn)自旋或互斥。
    下面就帶著這些問題,來總結(jié)一下多線程的相關(guān)問題。
iOS中的常見多線程方案
技術(shù)方案 簡介 語言 線程聲明周期 使用頻率
pthread 一套通用的多線程API,適用于Unix\Linux\windows等系統(tǒng),跨平臺,可移植,使用難度大 C 程序員管理 幾乎不用
NSThread 使用更加面向?qū)ο?,簡單易用,可直接操作線程對象 OC 程序員管理 偶爾使用
GCD 旨在替代NSThread等線程技術(shù),充分利用設(shè)備的多核 C 自動管理 經(jīng)常使用
NSOperation 基于GCD,比GCD多了一些更簡單實用的功能,使用更加面向?qū)ο?/td> OC 自動管理 經(jīng)常使用

GCD基礎(chǔ)回顧

GCD中有兩個用來執(zhí)行任務(wù)的函數(shù):

  • 用同步的方式執(zhí)行任務(wù)
    即在當前線程中去做事情
dispatch_sync(dispatch_queue_t  _Nonnull queue, ^{})
  • 用異步的方式執(zhí)行任務(wù)
    即另外開線程去做事情
dispatch_async(dispatch_queue_t  _Nonnull queue, ^{})
  • 并發(fā)隊列:
    可以讓多個任務(wù)同時執(zhí)行(自動開啟多個線程同時執(zhí)行任務(wù))
    并發(fā)功能只有在異步函數(shù)下才有效
  • 串行隊列:
    讓任務(wù)一個接著一個的執(zhí)行(只會開啟一條線程,一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù))
同步,異步,串行,并行
  • 同步和異步的主要影響:能不能開啟新的線程
    同步:在當前線程中執(zhí)行任務(wù),不具備開啟新線程的能力
    異步:在新的線程中執(zhí)行任務(wù)。具備開啟新線程的能力
  • 并發(fā)和和串行的主要影響:任務(wù)的執(zhí)行方法
    并發(fā):多個任務(wù)同時執(zhí)行(會開啟多條線程)
    串行:一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù)(只會開啟一個線程)
下面是各種隊列的執(zhí)行效果:
并發(fā)隊列 手動創(chuàng)建的串行隊列 主隊列
同步(sync) 沒有開啟新線程,串行執(zhí)行任務(wù) 沒有開啟新線程,串行執(zhí)行任務(wù) 沒有開啟新線程,串行執(zhí)行任務(wù)
異步(async) 有開啟新線程,并發(fā)執(zhí)行任務(wù) 有開啟新線程,串行執(zhí)行任務(wù) 沒有開啟新線程,串行執(zhí)行任務(wù)
GCD中死鎖的問題

面試題一:以下代碼在主線程中執(zhí)行,是否會產(chǎn)生死鎖?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //問題:以下代碼在主線程中執(zhí)行,會不會產(chǎn)生死鎖?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });
    
    NSLog(@"執(zhí)行任務(wù)3");
}

我們運行代碼,發(fā)現(xiàn)產(chǎn)生了崩潰,說明產(chǎn)生了死鎖。下面來分析一下為什么會產(chǎn)生死鎖:
我們知道,dispatch_sync()是同步執(zhí)行,不會開辟新線程,并且要dispatch_sync()的block執(zhí)行完了才會繼續(xù)往下執(zhí)行,所以任務(wù)2是加入到了主隊列中。主隊列是串行隊列,,所以會串行執(zhí)行加入其中的任務(wù),等一個任務(wù)執(zhí)行完了再執(zhí)行另外一個,在任務(wù)2加入主隊列之前,viewDidLoad這個大任務(wù)已經(jīng)加入了主隊列,所以任務(wù)2要等ViewDIdLoad執(zhí)行完,才會執(zhí)行任務(wù)2,也就是等到任務(wù)3執(zhí)行完,才會執(zhí)行任務(wù)2,但是由于任務(wù)3在任務(wù)2后面,所以要等到任務(wù)2執(zhí)行完了,才執(zhí)行任務(wù)3。這樣就造成了任務(wù)2等任務(wù)3,任務(wù)3等任務(wù)2,造成死鎖。
面試題二:以下代碼是否會發(fā)生死鎖:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //問題:以下代碼在主線程中執(zhí)行,會不會產(chǎn)生死鎖?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });
    
    //
    
    NSLog(@"執(zhí)行任務(wù)3");
}

運行程序,發(fā)現(xiàn)程序并沒有崩潰,并且產(chǎn)生打印:

2018-09-25 21:02:04.155692+0800 TEST[2610:87716] 執(zhí)行任務(wù)3
2018-09-25 21:02:04.168868+0800 TEST[2610:87716] 執(zhí)行任務(wù)2

為什么把同步改成異步,就不死鎖了呢?我們分析一下,由于隊列是主隊列,一定是把任務(wù)2加到主隊列中,并且在此之前viewDidLoad的任務(wù)已經(jīng)加入到了主隊列中,所以要viewDidLoad執(zhí)行完了才能執(zhí)行任務(wù)2,由于dispatch_async()是異步執(zhí)行,所以不用等到任務(wù)2執(zhí)行完了再執(zhí)行任務(wù)3,可以直接執(zhí)行任務(wù)3,任務(wù)3執(zhí)行完了,viewDidLoad也就執(zhí)行完了,也就可以執(zhí)行任務(wù)2了,所以打印的結(jié)果一定是先執(zhí)行任務(wù)3再執(zhí)行任務(wù)2。
面試題3:以下代碼是否會發(fā)生死鎖:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //問題:以下代碼在主線程中執(zhí)行,會不會產(chǎn)生死鎖?
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{//1
        NSLog(@"執(zhí)行任務(wù)2");
        
        dispatch_sync(queue, ^{
            NSLog(@"執(zhí)行任務(wù)3");
        });
        
        NSLog(@"執(zhí)行任務(wù)4");
    });
    
        NSLog(@"執(zhí)行任務(wù)3");
}

運行代碼,發(fā)現(xiàn)在dispatch_sync這里產(chǎn)生了崩潰,打印結(jié)果如下:

2018-09-25 21:15:47.637042+0800 TEST[2816:95982] 執(zhí)行任務(wù)3
2018-09-25 21:15:47.637048+0800 TEST[2816:96030] 執(zhí)行任務(wù)2

下面分析一下為什么會產(chǎn)生死鎖:
dispatch_async是異步執(zhí)行,所以先打印了執(zhí)行任務(wù)3,然后把1這個block加入了串行隊列中,這時串行隊列中有1這個block,然后又向串行隊列中加入了任務(wù)3,任務(wù)3需要同步執(zhí)行,所以任務(wù)3執(zhí)行完了才會執(zhí)行任務(wù)4,由于串行隊列中1這個block排在任務(wù)3前面,所以要1這個block完成才會執(zhí)行任務(wù)3,也就是要任務(wù)4執(zhí)行完了才會執(zhí)行任務(wù)3,而任務(wù)4又要等到任務(wù)3執(zhí)行完成,這樣互相等待,造成死鎖。

總結(jié)

造成死鎖的條件1是同步,2是往當前的串行隊列中添加事件。

直接獲取全局隊列和手動創(chuàng)建隊列的關(guān)系

看下面一段代碼:

    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_queue_t queue4 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue5 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"%p, %p, %p, %p, %p", queue1, queue2, queue3, queue4, queue5);

打印結(jié)果:

0x1062e2500, 0x1062e2500, 0x1062e2680, 0x600000146bf0, 0x600000146ca0

queue1,queue2,queue3是直接獲取的全局隊列,從打印結(jié)果可以看出,如果優(yōu)先級相同,則獲取的是同一個隊列,如果優(yōu)先級不同,則獲取的是不同的隊列。queue4,queue5是手動創(chuàng)建的隊列,即便它們的identifier相同,但是仍然創(chuàng)建了不同的隊列。

面試題

面試題一:問下列代碼的打印結(jié)果:

    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue1, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
- (void)test{
    
    NSLog(@"2");
}

我們先看一下打印結(jié)果:

1
3

很奇怪,2壓根就沒有打印,也就是壓根就沒有執(zhí)行test方法,這是為什么呢?
我們把

[self performSelector:@selector(test) withObject:nil afterDelay:.0];

改成

[self performSelector:@selector(test) withObject:nil];

看看打印結(jié)果:

1
2
3

說明這樣是能成功執(zhí)行test函數(shù)的,我們在runtime中找一下- (id)performSelector:(SEL)aSelector withObject:(id)object;的源碼:

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

可以看到,就是簡單的調(diào)用objc_msgSend()。但是在runtime的源碼中卻沒有找到- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;的實現(xiàn)。

我們再修改一下代碼,讓其直接在主線程中執(zhí)行:

        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");

打印結(jié)果:

1
2
3

那么就有理由猜測NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3");的執(zhí)行和線程有關(guān)。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];這句代碼的本質(zhì)是往runloop中去添加一個NSTimer,由于主線程中有runloop,所以可以正常執(zhí)行,但是在子線程中默認是沒有啟動runloop的,所以NSTimer也就沒有辦法成功執(zhí)行。

我們可以啟動子線程中的runloop試一下:

    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue1, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });

打印結(jié)果:

1
3
2
GNUstep

Foundation框架是不開源的,所以我們想看其中的源碼是看不到的。GNUstep是GNU計劃的項目之一,它將Cocoa的OC庫重新開源實現(xiàn)了一遍,雖然它不是蘋果官方的完整實現(xiàn),但是和官方的實現(xiàn)十分接近,區(qū)別不大,在學習的時候我們可以用來作為參考。
源碼地址:http://www.gnustep.org/resources/downloads.php

我們下載了GNUstep的代碼后,打開找到Foundation文件夾,在這個文件夾下找到NSRunLoop.m這個文件,在這個文件中找到- (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds這個方法的實現(xiàn):

- (void) performSelector: (SEL)aSelector
          withObject: (id)argument
          afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop     *loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer  *item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
                         target: self
                       argument: argument
                          delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

GCD隊列組

思考:如何用gcd實現(xiàn)以下功能:
異步并發(fā)執(zhí)行任務(wù)1,任務(wù)2
等任務(wù)1,任務(wù)2都執(zhí)行完畢后,再回到主線程執(zhí)行任務(wù)3
我們可以用dispatch_group_t和dispatch_group_notify來完成這個功能:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
        }
    });

如果需要在任務(wù)1和任務(wù)2完成之后再完成任務(wù)3,任務(wù)4,可以這樣:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)4-%@", [NSThread currentThread]);
        }
    });
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS多線程編程 基本知識 1. 進程(process) 進程是指在系統(tǒng)中正在運行的一個應(yīng)用程序,就是一段程序的執(zhí)...
    陵無山閱讀 6,335評論 1 14
  • 本文首發(fā)于我的個人博客:「程序員充電站」[https://itcharge.cn]文章鏈接:「傳送門」[https...
    ITCharge閱讀 350,584評論 308 1,928
  • 本文用來介紹 iOS 多線程中 GCD 的相關(guān)知識以及使用方法。這大概是史上最詳細、清晰的關(guān)于 GCD 的詳細講...
    花花世界的孤獨行者閱讀 575評論 0 1
  • 記得兒時,和父親最親密的接觸,就是趴在他的肩頭咬著手指的安逸,幸福。 漸漸長大的我,開始用我的眼光...
    瑩兒醉閱讀 223評論 0 0
  • 《隱》 大隱于朝,中隱于市,陶淵明“種菊東籬下,悠然見南山”式的歸隱山野,已然難以尋覓。 村莊曾是隱者的福地,自給...
    合肥張建春閱讀 369評論 0 1

友情鏈接更多精彩內(nèi)容