笔记 - 多线程之GCD

目录

  • 概念
  • 常用API的使用
  • 关于GCD的面试题

一、概念

  • 1.1、什么是GCD?

定义想执行的任务,并追加到适当的 Dispatch Queue中

  • 1.2、GCD的队列的概念
并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效


串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
  • 1.3、容易混淆的术语 (同步、异步、并发、串行)
同步和异步主要影响:能不能开启新线程
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力


并发和串行主要影响:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
各种队列的执行效果.png

二、常用API的使用

  • 2.1、dispatch_sync
dispatch_sync(<#dispatch_queue_t  _Nonnull queue#>,
              <#^(void)block#>)

用同步的方式执行任务
- 参数一、队列
- 参数二、任务
  • 2.2、dispatch_async
dispatch_async(<#dispatch_queue_t  _Nonnull queue#>,
               <#^(void)block#>)

- 参数一、队列
- 参数二、任务
  • 2.3、获取 Dispatch Queue

  • 2.3.1、通过API的生成(dispatch_queue_create)

dispatch_queue_create(<#const char * _Nullable label#>,
                      <#dispatch_queue_attr_t  _Nullable attr#>);

参数一:指定生成返回的Dispatch Queue的名称
- 推荐使用应用程序ID这种逆序全程域名,在Instruments及CrashLog中方便调试

参数二:指定生成返回的Dispatch Queue的类型
- 指定为 NULL 或 DISPATCH_QUEUE_SERIAL,生成Serial Dispatch Queue;
- 指定为DISPATCH_QUEUE_CONCURRENT,生成Concurrent Dispatch Queue
例:
dispatch_queue_t creatQueue1 = dispatch_queue_create("com.example.gcd.xxx",
                                                     NULL);
dispatch_queue_t creatQueue2 = dispatch_queue_create("com.example.gcd.xxx",
                                                     DISPATCH_QUEUE_SERIAL);
dispatch_queue_t creatQueue3 = dispatch_queue_create("com.example.gcd.xxx",
                                                     DISPATCH_QUEUE_CONCURRENT);
  • 2.3.2、获取系统提供的Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
- 在主线程中执行的 Dispatch Queue


dispatch_queue_t globeQueue = dispatch_get_global_queue(<#long identifier#>,
                                                        <#unsigned long flags#>)
- 所有应用程序都能够使用的 Concurrent Dispatch Queue
- 优先级:
#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  // 后台优先级
- ⚠️:通过XNU内核用于Global Dispatch Queue的线程不能保证实时性能,因此执行优先级只是大致的判断

使用:
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,
                                                        0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
                                                        0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,
                                                        0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,
                                                        0);
  • 2.4、dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                             (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)),
               dispatch_get_main_queue(), ^{
});

⚠️在多长时间之后执行,这是我们常用的API之一,现在说一下需要注意的点:
- dispatch_after函数不是在指定时间后执行处理,而是在指定时间追加处理到 Dispatch Queue;
- 本身存在延迟:执行的时间 = 传入参数 + runloop循环1次的时间 
  • 2.5、dispatch_once
- (instancetype)init {
    if (self = [super init]) {
        static int initialized = NO;
        if (initialized == NO) {
            // 初始化单例
        }
        initialized = YES;
    }
    return self;
}

- (instancetype)init {
    if (self = [super init]) {
        static dispatch_once_t pred;
        dispatch_once(&pred, ^{
            // 初始化单例
        });
    }
    return self;
}

第一段代码存在的隐患:
如果在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。
  • 2.6、Dispatch Group

使用场景:在追加到Dispatch Queue中多个处理全部结束后想执行结果处理。也就是说等多个异步线程执行完成后执行

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_async(queue, ^{
    NSLog(@"执行一");
});
dispatch_async(queue, ^{
    NSLog(@"执行二");
});    
dispatch_async(queue, ^{
    NSLog(@"执行三");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"三条线程执行完成");
});


2019-10-07 12:24:22.914339+0800 GCD[10825:325226] 执行二
2019-10-07 12:24:22.914345+0800 GCD[10825:325223] 执行一
2019-10-07 12:24:22.914369+0800 GCD[10825:325224] 执行三
2019-10-07 12:24:22.930029+0800 GCD[10825:325078] 三条线程执行完成

也可以使用 dispatch_group_wait函数仅等待全部处理执行结束

dispatch_group_wait(<#dispatch_group_t  _Nonnull group#>,
                    <#dispatch_time_t timeout#>)

- 参数一、传入线程组
- 参数二、指定等待的时间 (DISPATCH_TIME_FOREVER意味着永久等待)



dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_async(queue, ^{
    NSLog(@"执行一");
});
dispatch_async(queue, ^{
    NSLog(@"执行二");
});    
dispatch_async(queue, ^{
    NSLog(@"执行三");
});
sleep(3);

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"三条线程执行完成");
  • 2.7、dispatch_barrier_async(栅栏函数)

在进程管理中起到一个栅栏的作用,它等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,
⚠️该函数需要同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"执行一");
});
dispatch_async(queue, ^{
    NSLog(@"执行二");
});
dispatch_barrier_async(queue, ^{
    sleep(3);
    NSLog(@"xxxxxxxxxx");
});
dispatch_async(queue, ^{
    NSLog(@"执行三");
});
dispatch_async(queue, ^{
    NSLog(@"执行四");
});


2019-10-07 12:48:55.348575+0800 GCD[11221:340677] 执行一
2019-10-07 12:48:55.348582+0800 GCD[11221:340680] 执行二
2019-10-07 12:48:58.350728+0800 GCD[11221:340680] xxxxxxxxxx
2019-10-07 12:48:58.350999+0800 GCD[11221:340678] 执行四
2019-10-07 12:48:58.351001+0800 GCD[11221:340680] 执行三
  • 2.8、dispatch_set_target_queue
    更改Dispatch Queue的执行优先级

  • 2.9、dispatch_apply
    该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(5, queue, ^(size_t index) {
    NSLog(@"------%zu", index);
});
NSLog(@"执行完成");


2019-10-07 16:18:05.663250+0800 GCD[12009:372598] ------0
2019-10-07 16:18:05.663277+0800 GCD[12009:372679] ------1
2019-10-07 16:18:05.663277+0800 GCD[12009:372683] ------2
2019-10-07 16:18:05.663302+0800 GCD[12009:372678] ------3
2019-10-07 16:18:05.663446+0800 GCD[12009:372679] ------4
2019-10-07 16:18:05.663581+0800 GCD[12009:372598] 执行完成

⚠️:由于dispatch_apply函数也与 dispatch_sync函数相同,会等待处理执行结束,因此推荐在dispatch_async函数中非同步地执行dispatch_apply函数

  • 3.0、dispatch_suspenddispatch_resume
  • 3.1、Dispatch Semaphore
    持有计数信号

三、关于GCD的面试题

  • 你理解的多线程?
  • iOS的多线程方案有哪几种?你更倾向于哪一种?
  • 你在项目中用过GCD么
  • GCD的队列类型
  • 说一下OperationQueue和GCD的区别,以及各自的优势
  • 线程安全的处理手段有哪些?
  • OC你了解的锁有哪些?在你回答基础上进行二次提问?
    自旋锁和互斥锁对比?使用以上锁需要注意哪些?使用C/OC/C++,任选其一,实现自旋或互斥?口述即可
  • 3.1、iOS的多线程方案有哪几种?你更倾向于哪一种?
iOS中的常见多线程方案
  • 3.2、自旋锁、互斥锁比较
什么情况使用自旋锁比较划算?

- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张


什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
  • 3.3、关于死锁的相关面试题
NSLog(@"执行任务1");
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_sync(main, ^{
    NSLog(@"执行任务2");
});
NSLog(@"执行任务3");

2019-10-07 10:30:04.809292+0800 GCD[10060:304837] 执行任务1
❌产生死锁
NSLog(@"执行任务1");
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(main, ^{
    NSLog(@"执行任务2");
});
NSLog(@"执行任务3");

2019-10-07 10:29:03.893940+0800 GCD[10038:303891] 执行任务1
2019-10-07 10:29:03.894175+0800 GCD[10038:303891] 执行任务3
2019-10-07 10:29:03.918505+0800 GCD[10038:303891] 执行任务2
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.xxx", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    NSLog(@"执行任务2");
    dispatch_sync(queue, ^{
        NSLog(@"执行任务3");
    });
    NSLog(@"执行任务4");
});
NSLog(@"执行任务5");


2019-10-07 10:31:09.843690+0800 GCD[10091:306352] 执行任务1
2019-10-07 10:31:09.843905+0800 GCD[10091:306352] 执行任务5
2019-10-07 10:31:09.843937+0800 GCD[10091:306413] 执行任务2
❌产生死锁

通过上述代码我们可以得出一个结论:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列
如果某些场景需要这样做,可以使用两个队列来避免上面产生死锁问题。

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"执行任务2");
    dispatch_sync(queue2, ^{
        NSLog(@"执行任务3");
    });
    NSLog(@"执行任务4");
});
NSLog(@"执行任务5");

后续待扩展(线程锁)、GNUstep

这篇文章写的非常棒,可以看看 iOS多线程--彻底学会多线程之『GCD』

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

推荐阅读更多精彩内容