iOS探索 - 多线程之GCD应用

1,231 阅读14分钟

欢迎阅读iOS底层系列(建议按顺序)

iOS底层 - alloc和init探索

iOS底层 - 包罗万象的isa

iOS底层 - 类的本质分析

iOS底层 - cache_t流程分析

iOS底层 - 方法查找流程分析

iOS底层 - 消息转发流程分析

iOS底层 - dyld是如何加载app的

iOS底层 - 类的加载分析

iOS底层 - 分类的加载分析

iOS探索 - 多线程之相关原理

本文概述

本文主要在列出GCD使用过程中常用的API,在使用场景和注意细节加以说明。

1.多线程之GCD

1.1 为什么是GCD

线程编程有多种方式,pthreadNSThreadGCDNSOperation。为什么选择GCD?

方案简介语言生命周期
pthread通用API,跨平台,使用难度大C程序员管理
NSThread面向对象,简单易用,可直接操作线程OC程序员管理
GCD充分利用设备的多核C自动管理
NSOperation面向对象,基于GCD,功能更多OC自动管理

GCD是苹果为多核的并行运算提出的解决方案,它是一套纯C语言的API,它会自动利用更多的CPU内核,会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。

管理线程的生命周期是个棘手的活,而且容易出错,因此应尽可能避免使用。所以GCD和基于GCDNSOperation是日常开发中多线程的首选,程序员只需要告诉 GCD想要执行什么任务,不需要编写任何线程管理代码。

关于GCDNSOperation的选择:

NSOperation是对线程的高度抽象,子类化NSOperation 的设计思路,是具有面向对象的优点,且能自定义实现特殊需求,使得实现是多线程支持和接口简单的,会使项目的程序结构更好,建议在复杂项目中使用。

GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而 Block 参数的使用,会是代码更为易读,建议在简单项目中使用。

1.2 如何使用GCD

一句话概括,将任务添加到队列,并且指定执行任务的函数。这是使用GCD的核心思想。

这句话涉及任务队列函数三个概念:

  • 任务:就是block块内的代码,要被执行的部分
  • 队列:执行任务的等待队列,即用来存放任务的队列
  • 函数:是否可以在新的线程中执行任务,是否具备开启新线程的能力

1.3 队列和函数

串行队列(Serial)

  • 不开辟新线程,每次只有一个任务被执行,任务一个接着一个地执行。

并发队列(Concurrent)

  • 可以开启多个线程,让多个任务并发(同时)执行。

主队列(Main)

  • 在主线程执行任务的队列,代码默认都被添加到主队列
  • 本质上是串行队列

全局队列(Global)

  • 系统自身使用的队列,如果对队列没有特殊要求,在执行异步任务时,可以借用
  • 本质上是并发队列

同步函数(sync)

  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行
  • 只能在当前线程中执行任务,不具备开启新线程的能力

异步函数(async)

  • 异步添加任务到指定的队列中,不做任何等待,可以继续执行任务
  • 可以在新的线程中执行任务,具备开启新线程的能力
不同队列和不同函数的组合在不同线程具有不同的效果。

1.4 队列和函数的组合

全局队列类似于并发队列,不额外分析,所以有以下6种排列:

1.同步函数 + 串行队列
2.同步函数 + 并发队列
3.异步函数 + 串行队列
4.异步函数 + 并发队列
5.同步函数 + 主队列
6.异步函数 + 主队列

主线程不同队列不同函数的组合效果:

函数\队列串行队列并发队列主队列
同步函数没有开启线程,串行执行任务没有开启线程,串行执行任务死锁
异步函数有开启线程(1条),串行执行任务有开启线程,并发执行任务没有开启新线程,串行执行任务
  • 同步函数不会开启线程,且是串行执行任务
  • 异步函数不一定会开启线程,参考异步主队列
  • 主线程中,同步主队列会产生死锁,因为主队列中追加的同步任务主线程本身的任务两者之间相互等待,阻塞了主队列
  • 同步主队列不一定会产生死锁,比如在其他子线程
  • 死锁不一定只在主队列,其他的串行队列也可能

关于这几点后面的代码部分会进行验证。

基于同步并发异步串行可能使用较少,理解相对困难些,详细说明下:

同步并发是按顺序执行任务的但不开启线程。虽然并发队列可以同时执行多个任务,但同步函数不具备开启线程的能力,也就不会创新新线程,任务只能在一个线程执行。同一线程中只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作。所以就不存在并发,任务只能一个接一个按顺序执行,不能同时被执行。

异步串行是按顺序执行任务的且会开启一条线程。因为异步函数具备开启新线程的能力,但串行队列每次只有一个任务被执行,任务要一个接一个按顺序执行,使得其只开启一个线程。

简单来说,

同步并发因为没有开启新线程,导致任务都在一个线程执行,并发失去效果,任务按顺序执行

异步串行虽然开启了一个线程,但因为是串行,任务还是按顺序执行

虽然同步并发异步串行的任务都是按顺序进行,但原因有些区别,同步并发在于同步异步串行在于串行

2. API的使用说明

2.1 dispatch_block_t

dispatch_block_t 创建任务
② dispatch_queue_t 将任务添加到队列
③ dispatch_async 指定执行任务的函数

这是最能体现将任务添加到队列,并且指定执行任务的函数思想的三行代码。

不过一般合并表示:

dispatch_async(dispatch_queue_create("com.juejin.cn", NULL), ^{
    NSLog(@"hello word");
});

2.2 dispatch_queue_t

  • 主队列:使用dispatch_get_main_queue()获取,在主线程同步主队列会造成死锁
  • 全局队列:使用dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)获取,第一个参数为优先级,默认为DISPATCH_QUEUE_PRIORITY_DEFAULT,优先级如下:
#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
  • 串行队列:使用dispatch_queue_create("...",DISPATCH_QUEUE_SERIAL)创建,或者dispatch_queue_create("...",NULL)亦可
#define DISPATCH_QUEUE_SERIAL NULL
  • 并发队列:使用dispatch_queue_create("...",DISPATCH_QUEUE_CONCURRENT)创建

2.3 dispatch_sync和dispatch_async

同步 + 串行

dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
    dispatch_sync(queue, ^{
        NSLog(@"%d-%@",i,[NSThread currentThread]);
    });
}
------------------------------------------------------------
2020-12-09 16:01:41.064806+0800 001---函数与队列[26946:1461774] 0-<NSThread: 0x2801ef080>{number = 1, name = main}
2020-12-09 16:01:41.064905+0800 001---函数与队列[26946:1461774] 1-<NSThread: 0x2801ef080>{number = 1, name = main}
2020-12-09 16:01:41.064951+0800 001---函数与队列[26946:1461774] 2-<NSThread: 0x2801ef080>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行

同步 + 并发

dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
    dispatch_sync(queue, ^{
        NSLog(@"%d-%@",i,[NSThread currentThread]);
    });
}
------------------------------------------------------------
2020-12-09 16:06:04.335006+0800 001---函数与队列[27053:1463520] 0-<NSThread: 0x283627040>{number = 1, name = main}
2020-12-09 16:06:04.335107+0800 001---函数与队列[27053:1463520] 1-<NSThread: 0x283627040>{number = 1, name = main}
2020-12-09 16:06:04.335153+0800 001---函数与队列[27053:1463520] 2-<NSThread: 0x283627040>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行

异步 + 串行

dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
    dispatch_async(queue, ^{
        NSLog(@"%d-%@",i,[NSThread currentThread]);
    });
}
------------------------------------------------------------
2020-12-09 16:09:56.688148+0800 001---函数与队列[27187:1465347] 0-<NSThread: 0x282435900>{number = 6, name = (null)}
2020-12-09 16:09:56.688234+0800 001---函数与队列[27187:1465347] 1-<NSThread: 0x282435900>{number = 6, name = (null)}
2020-12-09 16:09:56.688281+0800 001---函数与队列[27187:1465347] 2-<NSThread: 0x282435900>{number = 6, name = (null)}
...
------------------------------------------------------------
开启一条线程,顺序执行

异步 + 并发

dispatch_queue_t queue = dispatch_queue_create("juejin",DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
    dispatch_async(queue, ^{
        NSLog(@"%d-%@",i,[NSThread currentThread]);
    });
}
------------------------------------------------------------
2020-12-09 16:12:47.086771+0800 001---函数与队列[27221:1466848] 0-<NSThread: 0x2827d8b40>{number = 6, name = (null)}
2020-12-09 16:12:47.086875+0800 001---函数与队列[27221:1466848] 2-<NSThread: 0x2827d8b40>{number = 6, name = (null)}
2020-12-09 16:12:47.086931+0800 001---函数与队列[27221:1466849] 1-<NSThread: 0x2827f74c0>{number = 4, name = (null)}
...
------------------------------------------------------------
开启多条线程,乱序执行

同步 + 主队列

主线程中,同步主队列会产生死锁的结论得以验证。


dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i<20; i++) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
});
------------------------------------------------------------
2020-12-09 16:18:29.902827+0800 001---函数与队列[27272:1468868] 0-<NSThread: 0x282d0a200>{number = 1, name = main}
2020-12-09 16:18:29.912577+0800 001---函数与队列[27272:1468868] 1-<NSThread: 0x282d0a200>{number = 1, name = main}
2020-12-09 16:18:29.913928+0800 001---函数与队列[27272:1468868] 2-<NSThread: 0x282d0a200>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行

子线程同步主队列不会产生死锁的结论得以验证。

异步 + 主队列

for (int i = 0; i<20; i++) {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"%d-%@",i,[NSThread currentThread]);
    });
}
------------------------------------------------------------
2020-12-09 16:31:28.193584+0800 001---函数与队列[27289:1470863] 0-<NSThread: 0x2818aaec0>{number = 1, name = main}
2020-12-09 16:31:28.193679+0800 001---函数与队列[27289:1470863] 1-<NSThread: 0x2818aaec0>{number = 1, name = main}
2020-12-09 16:31:28.193725+0800 001---函数与队列[27289:1470863] 2-<NSThread: 0x2818aaec0>{number = 1, name = main}
...
------------------------------------------------------------
不开启线程,顺序执行

主队列的任务都在主线程执行,所以异步函数也不一定会开启线程的结论得以验证。

(同异步)串行 + 同步串行


无论在子线程还是主线程,向同一串行队列追加同步串行,都会产生死锁。这是因为串行队列中追加的任务串行队列中原有的任务两者产生互相等待。

死锁不一定只在主队列,其他的串行队列也可能的结论得以验证。

2.4 dispatch_barrier_async 和 dispatch_barrier_sync

类似同步函数栅栏函数也能起到同步的效果。

同步的效果就意味着栅栏函数一般是搭配异步并发使用。

当使用dispatch_barrier_async时可得到以下结构:

dispatch_async(concurrentQueue, ^{
    子线程任务A
});
dispatch_barrier_async(concurrentQueue), ^{
});
主线程任务A
dispatch_async(concurrentQueue, ^{
    子线程任务B
});

输出顺序为:主线程任务A -> 子线程任务A -> 子线程任务B

当使用dispatch_barrier_sync时可得到以下结构:

dispatch_async(concurrentQueue, ^{
    子线程任务A
});
dispatch_barrier_sync(concurrentQueue), ^{
});
主线程任务A
dispatch_async(concurrentQueue, ^{
    子线程任务B
});

输出顺序为:子线程任务A -> 主线程任务A -> 子线程任务B

根据结果,可以得到以下结论:

  • 对于同一队列内的任务,能起到控制任务执行顺序,同步作用
  • dispatch_barrier_sync阻塞队列也阻塞线程dispatch_barrier_async只阻塞队列

注意细节

  • 异步栅栏函数阻塞的是队列,专队专用,非栅栏内的队列不会被阻塞。比如阻塞AFNetworking网络请求时往往不能成功,因为其内部的队列是自行创建的,需要获取它的队列才能阻塞
  • 全局队列的作用类似于并发队列,如果使用栅栏函数阻塞全局队列,在进行耗时任务时会导致程序崩溃。因为全局队列是系统使用的队列,app使用时也有很多系统任务在执行,阻塞系统队列会造成很多未知的后果

栅栏函数的相关特性做个总结:

  • 同步作用
  • 阻塞队列
  • 可选阻塞线程
  • 专队专用
  • 不用全局队列

2.5 dispatch_group_t

dispatch_group_t是提交到队列用于异步调用的一组块对象,内部维护未完成的关联任务的计数,在关联新任务时减少计数,在任务完成时增加计数,可以使用该计数来允许应用程序确定与组关联的所有任务何时完成。所以可以把相关的任务归并到一个组内来执行,通过监听组内所有任务的执行情况来做相应处理。

日常开发中,基于栅栏函数控制网络请求的顺序较麻烦,使用dispatch_group_t是可行且常见的方式。

dispatch_group_t相关的API包括:

dispatch_group_create

  • 创建可以将块对象分配给它的新调度组。

dispatch_group_async:

void dispatch_group_async(dispatch_group_t group,
                          dispatch_queue_t queue,
                          dispatch_block_t block);
  • 异步调度一个块以在指定队列执行,并同时将其与指定的调度组关联

dispatch_group_enter

void dispatch_group_enter(dispatch_group_t group);
  • 增加对应调度组中的未完成的关联任务,执行一次,任务计数减1(源码是-1)

dispatch_group_leave

void dispatch_group_leave(dispatch_group_t group);
  • dispatch_group_enter对应,减少对应调度组中的未完成的关联任务,执行一次,任务计数加1(源码是+1)。当计数为0时,dispatch_group_wait解除阻塞和dispatch_group_notify的block执行

dispatch_group_wait

long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
  • 会阻塞当前线程,等待调度组完成。当调度组完成时返回值为0或超时时间到达时返回值不为0,都会解除阻塞当前线程

dispatch_group_notify

void dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue, dispatch_block_t block);
  • dispatch_group_wait类似,调度组执行完毕时响应,但不会阻塞线程

注意细节

  • dispatch_group_notify自动控制任务计数,dispatch_group_enterdispatch_group_leave是手动控制任务计数。所以dispatch_group_notify使用更方便,dispatch_group_enterdispatch_group_leave更适合复杂环境。

  • dispatch_group_enterdispatch_group_leave需要成对出现,可先enter多次,但也要对应次数的leaveenter多于leave会导致notify不执行,leave多于enter会导致程序崩溃。

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        NSLog(@"第一个任务");
    });
    dispatch_group_async(group, queue, ^{
        dispatch_async(queue1, ^{
            NSLog(@"第二个任务");
        });
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"两个任务都完成了");
    });
    
-----------------------------------------------------------------

以上代码按认知来说,顺序应是第一个任务 -> 第二个任务 -> 两个任务都完成了

-----------------------------------------------------------------

实际情况:

2021-02-04 09:28:22.897517+0800 005---GCD进阶[11694:2915771] 第一个任务
2021-02-04 09:28:22.897753+0800 005---GCD进阶[11694:2915620] 两个任务都完成了
2021-02-04 09:28:22.897755+0800 005---GCD进阶[11694:2915769] 第二个任务

特别注意

  • 当组内任务是在子线程执行时,dispatch_group_notify在任务未完成时就会执行,此时需要手动控制计数防止出错

2.6 dispatch_once_t

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

});

dispatch_once_t在整个app运行期间只执行一次(理论如此),默认onceToken为0,执行一次后为-1

注意细节

  • dispatch_once_t不是一定只执行一次,如果需要再次执行则需要设置onceToken为0

使用场景

  • 单例的创建,保证只创建一次
  • Method Swizzling黑魔法,防止方法又被交换回来
  • 固定UI的构建阶段

2.7 dispatch_after

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

});

此函数将等待到指定的时间,然后异步地向指定队列添加任务。

使用场景:

  • 自动消失的弹窗
  • 防止按钮重复点击
  • 其他需要延时执行的场景

2.8 dispatch_semaphore_t

dispatch_semaphore_create:

dispatch_semaphore_t dispatch_semaphore_create(long value);
  • 创建dispatch_semaphore_t类型的信号量,创建的时候需要指定信号量的大小,且必须大于0

dispatch_semaphore_wait:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

该函数会对信号量做减1操作。如果减1后的值小于0,会一直等待,阻塞当前线程。

dispatch_semaphore_signal

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

该函数会对信号量做加1操作。

注意细节

  • dispatch_semaphore_waitdispatch_semaphore_signal是成对出现的。
  • 创建信号时,取值可以大于1,起到控制最大并发数的作用
  • dispatch_semaphore_t也是锁的一种形式

使用场景

  • 同步
  • 线程安全加锁
  • 控制最大并发数

2.9 dispatch_source_t

一种协调特定底层系统事件(如文件系统事件、计时器和UNIX信号)处理的对象。

在任一线程上调用它的一个函数dispatch_source_merge_data后,会执行 Dispatch Source事先定义好的句柄,这个过程叫Custom event用户事件,是 dispatch source支持处理的一种事件。

dispatch_source_create:

dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
	uintptr_t handle,
	unsigned long mask,
	dispatch_queue_t _Nullable queue)
  • 创建一个新的调度源来监控底层系统对象,并自动地向调度队列提交处理程序块以响应事件

dispatch_source_set_event_handler:

void dispatch_source_set_event_handler(dispatch_source_t source,
	dispatch_block_t _Nullable handler)
  • 创建事件源的回调

dispatch_source_merge_data

void dispatch_source_merge_data(dispatch_source_t source, unsigned long value)
  • 将数据合并到分派源并将其事件处理程序块提交到其目标队列。可以使用此函数来指示在一个应用程序定义的DISPATCH_SOURCE_TYPE_DATA_ADDDISPATCH_SOURCE_TYPE_DATA_OR类型的分派事件源上发生了事件。

dispatch_source_get_data

unsigned long dispatch_source_get_data(dispatch_source_t source)
  • 获取源事件数据

dispatch_resume

void dispatch_resume(dispatch_object_t object)
  • 继续源

dispatch_suspend

void dispatch_suspend(dispatch_object_t object)
  • 挂起源

使用场景

  • 基于源的定时器
  • 监听数据的改变

3.写在后面

GCDAPI远不止这些,以上仅列出了常用的部分。在使用API的过程中,如果知道其底层实现,用起来会更得心应手。

下一章是多线程系列之底层原理篇。敬请关注。