一、面试题解析
1.1 第一题
A、提问:
以下代码,会打印什么?
dispatch_queue_t queue = dispatch_queue_create("lj", DISPATCH_QUEUE_SERIAL); |
B、思路:
- 创建的事串行队列,所以队列依次进行
- 先执行
NSLog(@"1");
,打印1 - 接下来的异步函数
dispatch_async(queue, ^{//block1});
需要耗时,所以先跳过 - 执行
NSLog(@"5");
,打印5 - 回头来执行第3步异步函数内部的代码。
- 先执行
NSLog(@"2");
, 打印2 - 接下来执行同步函数
dispatch_sync(queue, ^{xxx});
, 此时阻塞线程。 - 后面的
NSLog(@"4");
需等待上一步同步函数执行完才能执行,无法执行打印。 - 异步函数内部
NSLog(@"3");
新创建,但是属于好事操作,需等待外部流程——即NSLog(@"4");
这一代码执行完。 - 上面两部互为等待,形成死锁。无法执行,会导致崩溃。
- 先执行
原理图可以这样看待:
C、回答:
打印1,5,2。以及崩溃
D、验证:
1.2 第二题(美团)
A、提问:
以下代码,会打印什么
__block int a = 0; |
B、思路:
- 首先执行的是异步函数,决定了每个任务都是独立、无线程阻塞、
放飞自我的 - 执行在全局并发队列里,即各自分别执行,会频繁读取
a++
,线程并不安全 - 上面第1、第2步决定了,当a自增到10时,跳出while 循环,但是打印的时候,很可能某1个或者多个任务还在异步执行,此时有两种情况
- 打印前a++ :此时很能有1个或多个任务在抢占资源,进行
a++
, 那么多次打印后打印 大于10 - 打印后a++ :即此时打印函数这个任务,成功抢占资源,先打印成功 则打印了10
- 打印前a++ :此时很能有1个或多个任务在抢占资源,进行
流程请参考图
C、回答:
打印结果 >= 10
D、验证
二、GCD 简介
2.1 概念
GCD 全称是Grand Central Dispatch,是苹果公司为多核的并行运算提出的解决方案,一种纯C 语言写成的框架,提供了非常多强大的函数
2.2 GCD 的优点
主要有以下几点
- GCD 会自动利用更多的CPU 内核(双核、四核等)
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。程序员只需要告诉GCD 执行的任务,不需要编写任何线程管理代码。
2.3 GCD 的函数
GCD 的函数使用过程为:将任务添加到队列,并且制定执行任务的函数
- 任务使用
block
封装 - 任务的
block
没有参数也没有返回值 - 函数的类型有异步函数和同步函数
2.3.1 异步函数 dispatch_async
- 不用等当前语句执行完毕,就可以执行下一条语句
- 会开启线程执行 block 的任务
- 异步是多线程的代名词
2.3.2 同步函数 dispatch_sync
- 必须等待当前语句执行完毕,才会执行下一条语句(俗称阻塞线程)
- 不会开启线程
- 在当前执行block 的任务
2.4. 队列
队列通常有并行队列和串行队列,顾名思义,并行队列队列宽度足够,互不干扰,各自为政;串行队列宽度只有1,每次只能执行同一个任务。
串行队列(Seiral Dispatch Queue):
每次只有一个任务被执行,任务一个接一个地执行。(只开启1个线程,一个任务执行完毕后,在执行下一个任务)
并发队列(Concurrent Dispatch Queue):
可以多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)。
注意:并发队列的并发功能只有在异步函数(async)下有效。
如下图所示
队列的概念主要两个重要的队列:
2.4.1 主队列
- 主队列专门用来在主线程上调度任务的队列,并不会开启线程
- 才用先进先出(FIFO)原则,主线程空闲时才会呆㷣度队列中的任务在主线程中执行
- 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度。
主队列的获取代码为:
dispatch_queue_t queue = dispatch_get_main_queue(); |
平时开发中的 UI 操作,都必须在主线程的主队列中进行。
2.4.2 全局队列
为了方便程序员自行使用其他队列,苹果提供了全局队列:dispatch_get_global_queue(0, 0)
,
而且全局队列是一个并发队列。
在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列。
2.5 函数与队列
函数(同步、异步)和队列(串行、并发)的组合一共有4种,主要有以下特点:
- 同步函数串行队列:
- 不会开启线程,在当前线程执行任务
- 任务串行执行,one by one
- 会产生线程阻塞
- 同步函数并发队列
- 不会开启线程,在当前线程执行任务
- 任务一个接一个,无阻塞
- 异步函数串行队列
- 开启一条新线程
- 任务一个接一个
- 异步函数并发队列
- 开启多个线程,在当前新线程执行任务
- 任务异步执行,没有顺序,由CPU 调度
2.6 死锁
主线程因为同步函数强制执行当前任务的特性,会让后面的任务等待。
而主队列等待主线程的任务执行完毕,才能执行自己的任务,最后导致主队列和主线程相互等待,造成死锁
举例如下:
- (void)test |
上文中代码,主队列必须执行dispatch_sync
了之后 才能继续执行后面的代码 但是主线程有任务在执行——正在执行test
解决方案:将任务同步添加到主队列当中:
- (void)test |
三、GCD 栅栏函数
3.1 概念
栅栏函数的作用,是控制任务执行的顺序,它是一种同步函数。
3.2 使用
一般有同步和异步两种
- 异步:
dispatch_barrier_async
代码以前的任务执行完,才会执行以后的任务 - 同步:
dispatch_barrier_sync
作用相同,但是执行这个会阻塞线程,影响后面的任务执行。
可见,同步的栅栏函数,也是另一种加锁的方式
3.3 举例
同步栅栏函数
异步栅栏函数
3.4 注意点
栅栏函数主要作用是同步效果
提高代码的安全性能,线程安全
基本原理是阻塞队列
使用自定义的队列,不可使用全局的,否则进程都会阻塞
使用同一并发队列,即目标与栅栏函数在同一个队列里。
比如AFN ,他会自行实现了一个内部的队列,保证代码的内聚。
四、GCD 调度组
4.1 概念
调度组的作用是:控制任务的执行顺序
4.2 调度
- 创建组:
dispatch_group_create
- 进组任务:
dispatch_group_async
。规则为先进组、后出组- 进组:
dispatch_group_enter
- 出组:
dispatch_group_leave
- 进组:
- 进组任务执行完毕通知:
dispatch_group_notify
- 进组任务执行等待时机:
dispatch_group_wait
4.3 使用
在日常业务开发中,可以将多个异步进行的事件放入group
中( 进组),待各自完成(出组)后,实现最终结果——是不是很熟悉,有时候多个token 进行拼接获取,最终得到可以使用的,这个流程就可以通过调度组实现。
- (void)test3{ |
最终打印结果为:
2020-05-12 14:58:02.479083+0800 ttttt[12127:739541] 当前a + b = 4 |
五、信号量
如何准确的打印第一题的数值,达到10的结果呢?
可以使用信号量,可以有效的避免异步函数造成任务不可控。主要流程有如下几点
- 创建信号量
- 信号等待,即锁住线程。信号量–
- 执行业务,解锁开关,即业务不执行完,线程无法往后走。信号量++
如下:
// 1. 创建信号量 |
成功打印结果如下:
五、GCD 的Dispatch_source
5.1 概念
An object that coordinates the processing of specific low-level system events, such as file-system events, timers, and UNIX signals.
Gispatch source 是一种协调特定的低级别系统事件,如文件系统事件、Timer以及UNIX 信号。
5.2 特点
CPU 符合非常小,不占用资源
联合体,结构简洁,使用更高效
5.3 使用
创建源 : 创建一个dispatch source 来监测低级别的系统事件。具体实现为
dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue);
- type - dispatch source 的类型。举个例子,创建 定时器source,指定为
DISPATCH_SOURCE_TYPE_TIMER
- handle - 监控回调的句柄
- mask - 决定哪些事件是需要的flags 的掩码。
- queue - 事件回调block 需要提交的目标队列
- type - dispatch source 的类型。举个例子,创建 定时器source,指定为
管理事件回调
代码水岸为:
void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t handler);
获取源 属性
合并数据到一个分发的源,并提交它的事件回调block 到自己的目标队列中。
代码实现为:
void dispatch_source_merge_data(dispatch_source_t source, unsigned long value);
取消源
使用方式为
void dispatch_source_cancel(dispatch_source_t source);
5.4 应用
5.4.1 代码实现
创建队列
self.queue = dispatch_queue_create("lj", 0);
创建源
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
配置事件回调
dispatch_source_set_event_handler(self.source, ^{
NSUInteger value = dispatch_source_get_data(self.source);
NSLog(@"获取到 %lu", (unsigned long)value);
});激活源
dispatch_resume(self.source);
提交时间回调到目标队列中
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.value += 1;
dispatch_source_merge_data(self.source, self.value);
}
5.4.2 运行查看
由于是点击事件触发,所以依次增加,结果为:
2020-05-12 15:37:25.606845+0800 ttttt[12489:768578] 获取到 1 |
六、总结
在这一章节,主要复习了GCD 的各种应用,认识了队列与函数,以及各种加锁的方式(栅栏函数等),这里有个小小的练习:自定义的source 写的Timer,供参考。