iOS源码解析:多线程<一>

iOS开发中经常要使用到多线程,在面试的时候也是经常问到,比较常见的面试题有下面这些:

  • iOS的多线程方案有哪几种?你更倾向于哪一种?
  • GCD的队列类型。
  • 说一下OperationQueue和GCD的区别,以及各自的优势。
  • 线程安全的处理手段有哪些?
  • OC你了解的锁有哪些?在此基础上进行二次提问“
    1.自旋和互斥对比
    2.使用以上锁需要注意哪些?
    3.用C/OC/C++,任选其一,实现自旋或互斥。
    下面就带着这些问题,来总结一下多线程的相关问题。
iOS中的常见多线程方案
技术方案 简介 语言 线程声明周期 使用频率
pthread 一套通用的多线程API,适用于Unix\Linux\windows等系统,跨平台,可移植,使用难度大 C 程序员管理 几乎不用
NSThread 使用更加面向对象,简单易用,可直接操作线程对象 OC 程序员管理 偶尔使用
GCD 旨在替代NSThread等线程技术,充分利用设备的多核 C 自动管理 经常使用
NSOperation 基于GCD,比GCD多了一些更简单实用的功能,使用更加面向对象 OC 自动管理 经常使用

GCD基础回顾

GCD中有两个用来执行任务的函数:

  • 用同步的方式执行任务
    即在当前线程中去做事情
dispatch_sync(dispatch_queue_t  _Nonnull queue, ^{})
  • 用异步的方式执行任务
    即另外开线程去做事情
dispatch_async(dispatch_queue_t  _Nonnull queue, ^{})
  • 并发队列:
    可以让多个任务同时执行(自动开启多个线程同时执行任务)
    并发功能只有在异步函数下才有效
  • 串行队列:
    让任务一个接着一个的执行(只会开启一条线程,一个任务执行完毕后,再执行下一个任务)
同步,异步,串行,并行
  • 同步和异步的主要影响:能不能开启新的线程
    同步:在当前线程中执行任务,不具备开启新线程的能力
    异步:在新的线程中执行任务。具备开启新线程的能力
  • 并发和和串行的主要影响:任务的执行方法
    并发:多个任务同时执行(会开启多条线程)
    串行:一个任务执行完毕后,再执行下一个任务(只会开启一个线程)
下面是各种队列的执行效果:
并发队列 手动创建的串行队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务
异步(async) 有开启新线程,并发执行任务 有开启新线程,串行执行任务 没有开启新线程,串行执行任务
GCD中死锁的问题

面试题一:以下代码在主线程中执行,是否会产生死锁?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //问题:以下代码在主线程中执行,会不会产生死锁?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"执行任务2");
    });
    
    NSLog(@"执行任务3");
}

我们运行代码,发现产生了崩溃,说明产生了死锁。下面来分析一下为什么会产生死锁:
我们知道,dispatch_sync()是同步执行,不会开辟新线程,并且要dispatch_sync()的block执行完了才会继续往下执行,所以任务2是加入到了主队列中。主队列是串行队列,,所以会串行执行加入其中的任务,等一个任务执行完了再执行另外一个,在任务2加入主队列之前,viewDidLoad这个大任务已经加入了主队列,所以任务2要等ViewDIdLoad执行完,才会执行任务2,也就是等到任务3执行完,才会执行任务2,但是由于任务3在任务2后面,所以要等到任务2执行完了,才执行任务3。这样就造成了任务2等任务3,任务3等任务2,造成死锁。
面试题二:以下代码是否会发生死锁:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //问题:以下代码在主线程中执行,会不会产生死锁?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"执行任务2");
    });
    
    //
    
    NSLog(@"执行任务3");
}

运行程序,发现程序并没有崩溃,并且产生打印:

2018-09-25 21:02:04.155692+0800 TEST[2610:87716] 执行任务3
2018-09-25 21:02:04.168868+0800 TEST[2610:87716] 执行任务2

为什么把同步改成异步,就不死锁了呢?我们分析一下,由于队列是主队列,一定是把任务2加到主队列中,并且在此之前viewDidLoad的任务已经加入到了主队列中,所以要viewDidLoad执行完了才能执行任务2,由于dispatch_async()是异步执行,所以不用等到任务2执行完了再执行任务3,可以直接执行任务3,任务3执行完了,viewDidLoad也就执行完了,也就可以执行任务2了,所以打印的结果一定是先执行任务3再执行任务2。
面试题3:以下代码是否会发生死锁:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //问题:以下代码在主线程中执行,会不会产生死锁?
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{//1
        NSLog(@"执行任务2");
        
        dispatch_sync(queue, ^{
            NSLog(@"执行任务3");
        });
        
        NSLog(@"执行任务4");
    });
    
        NSLog(@"执行任务3");
}

运行代码,发现在dispatch_sync这里产生了崩溃,打印结果如下:

2018-09-25 21:15:47.637042+0800 TEST[2816:95982] 执行任务3
2018-09-25 21:15:47.637048+0800 TEST[2816:96030] 执行任务2

下面分析一下为什么会产生死锁:
dispatch_async是异步执行,所以先打印了执行任务3,然后把1这个block加入了串行队列中,这时串行队列中有1这个block,然后又向串行队列中加入了任务3,任务3需要同步执行,所以任务3执行完了才会执行任务4,由于串行队列中1这个block排在任务3前面,所以要1这个block完成才会执行任务3,也就是要任务4执行完了才会执行任务3,而任务4又要等到任务3执行完成,这样互相等待,造成死锁。

总结

造成死锁的条件1是同步,2是往当前的串行队列中添加事件。

直接获取全局队列和手动创建队列的关系

看下面一段代码:

    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);

打印结果:

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

queue1,queue2,queue3是直接获取的全局队列,从打印结果可以看出,如果优先级相同,则获取的是同一个队列,如果优先级不同,则获取的是不同的队列。queue4,queue5是手动创建的队列,即便它们的identifier相同,但是仍然创建了不同的队列。

面试题

面试题一:问下列代码的打印结果:

    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");
}

我们先看一下打印结果:

1
3

很奇怪,2压根就没有打印,也就是压根就没有执行test方法,这是为什么呢?
我们把

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

改成

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

看看打印结果:

1
2
3

说明这样是能成功执行test函数的,我们在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);
}

可以看到,就是简单的调用objc_msgSend()。但是在runtime的源码中却没有找到- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;的实现。

我们再修改一下代码,让其直接在主线程中执行:

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

打印结果:

1
2
3

那么就有理由猜测NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3");的执行和线程有关。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];这句代码的本质是往runloop中去添加一个NSTimer,由于主线程中有runloop,所以可以正常执行,但是在子线程中默认是没有启动runloop的,所以NSTimer也就没有办法成功执行。

我们可以启动子线程中的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]];
    });

打印结果:

1
3
2
GNUstep

Foundation框架是不开源的,所以我们想看其中的源码是看不到的。GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然它不是苹果官方的完整实现,但是和官方的实现十分接近,区别不大,在学习的时候我们可以用来作为参考。
源码地址:http://www.gnustep.org/resources/downloads.php

我们下载了GNUstep的代码后,打开找到Foundation文件夹,在这个文件夹下找到NSRunLoop.m这个文件,在这个文件中找到- (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds这个方法的实现:

- (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实现以下功能:
异步并发执行任务1,任务2
等任务1,任务2都执行完毕后,再回到主线程执行任务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(@"任务1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });

如果需要在任务1和任务2完成之后再完成任务3,任务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(@"任务1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务4-%@", [NSThread currentThread]);
        }
    });
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容

  • iOS多线程编程 基本知识 1. 进程(process) 进程是指在系统中正在运行的一个应用程序,就是一段程序的执...
    陵无山阅读 6,043评论 1 14
  • 本文首发于我的个人博客:「程序员充电站」[https://itcharge.cn]文章链接:「传送门」[https...
    ITCharge阅读 347,912评论 308 1,926
  • 本文用来介绍 iOS 多线程中 GCD 的相关知识以及使用方法。这大概是史上最详细、清晰的关于 GCD 的详细讲...
    花花世界的孤独行者阅读 500评论 0 1
  • 记得儿时,和父亲最亲密的接触,就是趴在他的肩头咬着手指的安逸,幸福。 渐渐长大的我,开始用我的眼光...
    莹儿醉阅读 196评论 0 0
  • 《隐》 大隐于朝,中隐于市,陶渊明“种菊东篱下,悠然见南山”式的归隐山野,已然难以寻觅。 村庄曾是隐者的福地,自给...
    合肥张建春阅读 332评论 0 1