【底层探索】- 多线程(二)GCD应用


一、面试题解析

1.1 第一题

A、提问:

以下代码,会打印什么?

dispatch_queue_t queue = dispatch_queue_create("lj", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{//block1
NSLog(@"2");
dispatch_sync(queue, ^{//block2
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");

B、思路:

  1. 创建的事串行队列,所以队列依次进行
  2. 先执行 NSLog(@"1");打印1
  3. 接下来的异步函数dispatch_async(queue, ^{//block1}); 需要耗时,所以先跳过
  4. 执行 NSLog(@"5");打印5
  5. 回头来执行第3步异步函数内部的代码。
    1. 先执行NSLog(@"2");打印2
    2. 接下来执行同步函数 dispatch_sync(queue, ^{xxx});, 此时阻塞线程。
    3. 后面的NSLog(@"4"); 需等待上一步同步函数执行完才能执行,无法执行打印。
    4. 异步函数内部NSLog(@"3"); 新创建,但是属于好事操作,需等待外部流程——即NSLog(@"4"); 这一代码执行完。
    5. 上面两部互为等待,形成死锁。无法执行,会导致崩溃。

原理图可以这样看待:

C、回答:

打印1,5,2。以及崩溃

D、验证:

1.2 第二题(美团)

A、提问:

以下代码,会打印什么

__block int a = 0;
while (a < 10) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
});
}
NSLog(@"a = %d \n", a);

B、思路:

  1. 首先执行的是异步函数,决定了每个任务都是独立、无线程阻塞放飞自我的
  2. 执行在全局并发队列里,即各自分别执行,会频繁读取a++,线程并不安全
  3. 上面第1、第2步决定了,当a自增到10时,跳出while 循环,但是打印的时候,很可能某1个或者多个任务还在异步执行,此时有两种情况
    1. 打印前a++ :此时很能有1个或多个任务在抢占资源,进行a++, 那么多次打印后打印 大于10
    2. 打印后a++ :即此时打印函数这个任务,成功抢占资源,先打印成功 则打印了10

流程请参考图

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_queue_t q = dispatch_get_main_queue();

NSLog(@"!!!!");

dispatch_sync(q, ^{
NSLog(@"%@",[NSThread currentThread]);
});
}

上文中代码,主队列必须执行dispatch_sync了之后 才能继续执行后面的代码 但是主线程有任务在执行——正在执行test

解决方案:将任务同步添加到主队列当中:

- (void)test
{
dispatch_queue_t q = dispatch_get_main_queue();

dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_sync(q, ^{
NSLog(@"%@",[NSThread currentThread]);
});
});
}

三、GCD 栅栏函数

3.1 概念

栅栏函数的作用,是控制任务执行的顺序,它是一种同步函数。

3.2 使用

一般有同步和异步两种

  • 异步: dispatch_barrier_async 代码以前的任务执行完,才会执行以后的任务
  • 同步:dispatch_barrier_sync 作用相同,但是执行这个会阻塞线程,影响后面的任务执行。

可见,同步的栅栏函数,也是另一种加锁的方式

3.3 举例

  • 同步栅栏函数

  • 异步栅栏函数

3.4 注意点

  • 栅栏函数主要作用是同步效果

  • 提高代码的安全性能,线程安全

  • 基本原理是阻塞队列

  • 使用自定义的队列,不可使用全局的,否则进程都会阻塞

  • 使用同一并发队列,即目标与栅栏函数在同一个队列里。

    比如AFN ,他会自行实现了一个内部的队列,保证代码的内聚。

四、GCD 调度组

4.1 概念

调度组的作用是:控制任务的执行顺序

4.2 调度

  1. 创建组:dispatch_group_create
  2. 进组任务: dispatch_group_async。规则为先进组、后出组
    1. 进组:dispatch_group_enter
    2. 出组:dispatch_group_leave
  3. 进组任务执行完毕通知:dispatch_group_notify
  4. 进组任务执行等待时机:dispatch_group_wait

4.3 使用

在日常业务开发中,可以将多个异步进行的事件放入group 中( 进组),待各自完成(出组)后,实现最终结果——是不是很熟悉,有时候多个token 进行拼接获取,最终得到可以使用的,这个流程就可以通过调度组实现。

- (void)test3{
dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
__block int a = 0;
__block int b = 0;
dispatch_group_async(group, queue, ^{
a += 2;
dispatch_group_enter(group);
});
dispatch_group_leave(group);

dispatch_group_async(group, queue, ^{
b += 2;
dispatch_group_enter(group);
});
dispatch_group_leave(group);

dispatch_group_notify(group, queue, ^{
NSLog(@"当前a + b = %d", a + b);
});

}

最终打印结果为:

2020-05-12 14:58:02.479083+0800 ttttt[12127:739541] 当前a + b = 4

五、信号量

如何准确的打印第一题的数值,达到10的结果呢?

可以使用信号量,可以有效的避免异步函数造成任务不可控。主要流程有如下几点

  • 创建信号量
  • 信号等待,即锁住线程。信号量–
  • 执行业务,解锁开关,即业务不执行完,线程无法往后走。信号量++

如下:

  // 1. 创建信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(1);

__block int a = 0;
while (a < 10) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
NSLog(@"a = %d \n", a);
// 解锁线程
dispatch_semaphore_signal(sem);
});
// 等待信号、锁住线程
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}

成功打印结果如下:

五、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 使用

  1. 创建源 : 创建一个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 需要提交的目标队列
  2. 管理事件回调

    代码水岸为:

    void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t handler);
  3. 获取源 属性

    合并数据到一个分发的源,并提交它的事件回调block 到自己的目标队列中。

    代码实现为:

    void dispatch_source_merge_data(dispatch_source_t source, unsigned long value);
  4. 取消源

    使用方式为

    void dispatch_source_cancel(dispatch_source_t source);

5.4 应用

5.4.1 代码实现

  1. 创建队列

    self.queue = dispatch_queue_create("lj", 0);
  2. 创建源

    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
  3. 配置事件回调

    dispatch_source_set_event_handler(self.source, ^{
    NSUInteger value = dispatch_source_get_data(self.source);
    NSLog(@"获取到 %lu", (unsigned long)value);
    });
  4. 激活源

    dispatch_resume(self.source);
  5. 提交时间回调到目标队列中

    - (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
2020-05-12 15:37:26.336439+0800 ttttt[12489:768578] 获取到 2
2020-05-12 15:37:26.936856+0800 ttttt[12489:768578] 获取到 3
2020-05-12 15:37:27.689042+0800 ttttt[12489:768578] 获取到 4
2020-05-12 15:37:29.186446+0800 ttttt[12489:768578] 获取到 5

**

六、总结

在这一章节,主要复习了GCD 的各种应用,认识了队列与函数,以及各种加锁的方式(栅栏函数等),这里有个小小的练习:自定义的source 写的Timer,供参考。

Github Demo地址


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【底层探索】- 多线程(一) 【底层探索】- 多线程(一)
一、线程定义1.1 基本概念 线程(Thread),有时被称为轻量级进程(Lightweight Progress, LWP),是程序执行流的最小单位。 一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。通常意义上,一
2020-04-06 李佳
下一篇 
【底层探索】- KVO(下)自定义KVO 【底层探索】- KVO(下)自定义KVO
序上文中,初步探索了KVO的使用以及背后的实现原理,这次来自己实现一个KVO,一方面在过程中加深理解,另一方面可以感受一下使用场景。 根据上文总结,KVO的实现一共有3部分: 添加KVO监听方法 KVO 通知实现方法 KVO 移除方法
2020-04-04 李佳
  目录