阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)
多线程
撸面试题中,文中内容基本上都是搬运自大佬博客及自我理解,可能有点乱,不喜勿喷!!!
主要以GCD为主
1、iOS开发中有多少类型的线程?分别对比
- Pthreads : 跨系统 c 语言多线程框架,不推荐。
- NSThread : ## 面向对象,需手动管理生命周期。
- GCD : Grand Central Dispatch,主打任务与队列,告诉他要做什么即可。
- NSOperation & NSOperationQueue : GCD 的封装,面向对象
2、GCD有哪些队列,默认提供哪些队列
-
主队列
dispatch_get_main_queue()
-
全局并发队列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
队列优先级从高到底为:
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
-
自定义队列(串行 Serial 与并行 Concurrent)
dispatch_queue_create("这里是队列名字", DISPATCH_QUEUE_SERIAL)
串行
DISPATCH_QUEUE_SERIAL
并行
DISPATCH_QUEUE_CONCURRENT
3、GCD有哪些方法api
- 队列
dispatch_get_main_queue()
dispatch_get_global_queue()
dispatch_queue_create()
- 执行
dispatch_async()
dispatch_sync()
dispatch_after()
dispatch_once()
dispatch_apply()
dispatch_barrier_async()
dispatch_barrier_sync()
- 调度组
dispatch_group_create()
dispatch_group_async()
dispatch_group_enter()
dispatch_group_leave()
dispatch_group_notify()
dispatch_group_wait()
- 信号量=
dispatch_semaphore_create()
dispatch_semaphore_wait()
dispatch_semaphore_signal()
- 调度资源
dispatch_source_create()
dispatch_source_set_timer()
dispatch_source_set_event_handler()
dispatch_resume()
dispatch_suspend()
dispatch_source_cancel()
dispatch_source_testcancel()
dispatch_source_set_cancel_handler()
4、GCD主线程 & 主队列的关系
提交到主队列的任务在主线程执行。
5、如何实现同步,有多少方式就说多少
dispatch_sync()
dispatch_barrier_sync()
dispatch_group_create() + dispatch_group_wait()
dispatch_apple()
dispatch_semaphore_create() + dispatch_semaphore_wait()
[NSOpertaion start]
NSOperationQueue.maxConcurrentOperationCount = 1
锁 pthread_mutex
NSLock
NSRecursiveLock
NSConditionLock & NSCondition
6、dispatch_once实现原理
-
读取 token 值
dispatch_once_t.dgo_once
; -
若 Block 已完成,return;
-
若 Block 没有完成,尝试原子性修改
dispatch_once_t.dgo_once
值为DLOCK_ONCE_UNLOCKED
;3.1 修改成功,执行 Block,原子性修改
dispatch_once_t.dgo_once
为DLOCK_ONCE_DONE
; 然后唤醒等待的线程3.2 若失败,则进入循环等待
7、什么情况下会死锁
A 等 B,B 等 A。
8、有哪些类型的线程锁,分别介绍下作用和使用场景
锁 | 种类 | 备注 |
---|---|---|
OSSpinLock |
自旋锁 | 不安全,iOS 10 已启用 |
os_unfair_lock |
互斥锁 | 替代 OSSpinLock
|
pthread_mutex |
互斥锁 |
PTHREAD_MUTEX_NORMAL 、#import <pthread.h>
|
pthread_mutex (recursive) |
递归锁 |
PTHREAD_MUTEX_RECURSIVE 、#import <pthread.h>
|
pthread_mutex (cond) |
条件锁 |
pthread_cond_t 、 #import <pthread.h>
|
pthread_rwlock |
读写锁 | 读操作重入,写操作互斥 |
@synchronized |
互斥锁 | 性能差,且无法锁住内存地址更改的对象 |
NSLock |
互斥锁 | 封装 pthread_mutex
|
NSRecursiveLock |
递归锁 | 封装 pthread_mutex (recursive)
|
NSCondition |
条件锁 | 封装 pthread_mutex (cond)
|
NSConditionLock |
条件锁 | 可以指定具体条件值 |
9、NSOperationQueue中的maxConcurrentOperationCount默认值
-1。这个值使系统根据系统条件而设置最大值
10、NSTimer、CADisplayLink、dispatch_source_t 的优劣
优点 | 缺点 | ||
---|---|---|---|
NSTimer |
使用简单 | 依赖 Runloop,具体表现在 无 Runloop 无法使用、NSRunLoopCommonModes 、不精确 |
加入到 主线程 中 |
CADisplaylink |
依赖屏幕刷新频率出发事件,最精确。最合适做 UI 刷新。 | 若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承 | |
dispatch_source_t |
不依赖 Runloop | 并不精确,使用相对麻烦 |
-
NSTimer
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; [timer invalidate];
-
CADisplaylink
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(takeTimer:)]; [link addToRunLoop:[NSRunLodop currentRunLoop] forMode:NSRunLoopCommonModes]; link.paused = !link.paused; [link invalidate];
-
dispatch_source_t
: 具体查看2.10 dispatch_source
__block int countDown = 6; /// 创建 计时器类型 的 Dispatch Source dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); /// 配置这个timer dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0); /// 设置 timer 的事件处理 dispatch_source_set_event_handler(timer, ^{ //定时器触发时执行 if (countDown <= 0) { dispatch_source_cancel(timer); NSLog(@"倒计时 结束 ~~~"); } else { NSLog(@"倒计时还剩 %d 秒...", countDown); } countDown--; }); /// 启动 timer dispatch_resume(timer);
搞事情~~~
面试资料:
面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
面试题资料或者相关学习资料都在群文件中 进群即可下载!
一、 NSThread
NSThread 是苹果封装过的,面向对象。可以使用它直接操作线程,但需要开发者手动管理其生命周期。
但是相比于 GCD 与 NSOperation / NSOperationQueue 来说更加轻量。
1 创建 NSThread
在 iOS 10之前:
// 创建 NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
// 启动 thread
[thread start];
// 创建并启动线程 - ;类方法
[NSThread detachNewThreadSelector:@selector(doSomething) toTarget:self withObject:nil];
复制代码
在 iOS 10之后,苹果贴心地为我们准备了 Block 的回调方式:
- (instancetype)initWithBlock:(void (^)(void))block;
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
复制代码
除了显示创建线程实例之外,Apple 还为我们提供多种 NSObject 的分类方法来使用,具体函数详见 NSObject - Objective-C Runtime 分类 Sending Messages
2 NSThread 常用方法
NSThread 的方法虽说不多,但其实也不少
- 常用的类方法与类属性:
// 获取当前线程,只读类属性
@property (class, readonly, strong) NSThread *currentThread;
// 若 number 为 1,则证明为主线程
// <NSThread: 0x281dd6100>{number = 1, name = main}
// 获取主线程,只读类属性
@property (class, readonly, strong) NSThread *mainThread;
// 判断当前线程是否是主线程,只读类属性
@property (class, readonly) BOOL isMainThread;
// 休眠一定时间
+ (void)sleepUntilDate:(NSDate *)date;
// 休眠到特定时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 退出当前线程
+ (void)exit;
- 常用的实例方法与实例属性:
// 线程名
@property (nullable, copy) NSString *name;
// 是否正在执行任务
@property (readonly, getter=isExecuting) BOOL executing;
// 是否已执行结束
@property (readonly, getter=isFinished) BOOL finished;
// 是否已被取消(一旦被取消,则该线程应 exit)
@property (readonly, getter=isCancelled) BOOL cancelled;
// 是否为主线程
@property (readonly) BOOL isMainThread;
// 取消线程(一旦取消,则该线程应 exit)
- (void)cancel;
// 启动线程
- (void)start;
二、GCD
通常情况,系统都会允许应用提交异步请求,然后系统处理请求的过程中,应用可以继续处理自己的事情。
GCD 便是基于这个准则而设计。
Dispatch - Apple 中这样介绍 GCD:
Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.
通过向系统管理的
dispatch queues
提交工作来在多核硬件上并发执行代码。
GCD,是 iOS 中多线程编程使用最多也是最方便的解决方案。
使用 GCD 有如下好处:
- GCD 会自动使用更多的 CPU 内核;
- GCD 自动管理线程的生命周期;
- GCD 能通过延迟昂贵计算任务并在后台运行来改善应用的相应性能;
- GCD 提供了一个易于使用的并发模型(不仅仅是线程与锁);
- 开发者只需要告诉 GCD 该干什么,无需多余的线程管理代码;
1、任务与队列
GCD 中有两个重要概念:任务 和 队列。
1.1 任务
执行的操作,也就是使用 GCD 时 Block 中需要执行的那段代码。
我个人理解,任何一句代码都是一个任务。比如 int a = 1
或者 NSLog(@"log")
;
执行任务有两种方式:同步执行(dispatch_sync
与 异步执行(dispatch_async
)。两者区别在于是否会阻塞当前线程以及是否具有开启新线程的能力。
-
同步执行:阻塞当前线程并等待 Block 中的任务执行完成,然后当前线程才会继续往后执行。不具备开启新线程的能力。
-
异步执行:不阻塞当前线程,当前线程直接往后执行。具备开启新线程的能力,但不一定会开启新线程。
1.2 队列
存放任务的队列。队列是一种特殊的线性表,采用先进先出(FIFO
)的规则。
也就是说,新加入的任务总是被插入到队列的末尾,但执行任务是从队列头开始的。这就跟日常生活中的排队一样。
队列分为 串行队列(Serial Dispatch Queue) 和 并行队列(Concurrent Dispatch Queue)。
-
串行队列中的任务按照 FIFO 的顺序取出并执行,前一个任务执行完才会取出下一个。
-
并行队列中的任务也是按照 FIFO 的顺序取出,但是 GCD 会开启新的线程来执行取出的任务。
这个取出任务并开启新线程执行的动作非常快,所以看起来就像是任务一起执行的。
但是,如果队列中的任务数量过大,GCD 也不可能开启一万条线程同时执行任务的。
同时,并对队列的并发功能只在 异步执行 时有效。
串行队列与并行队列的区别可以使用 这篇博客 的两张图来说明:
GCD 公开有五中不同的队列:主线程的 main queue,3个不同优先级的后台队列,一个优先级更低的后台队列(用于 I/O)
同时,用户还可以创建自定义队列,串行队列或并行队列都可以。在自定义队列中被调度的所有 Block 最终都将放入到系统的全局队列和线程池中。
复制一张 大佬的图
同步执行 | 异步执行 | |
---|---|---|
串行队列 | 当前线程,一个一个执行 | 其他线程,一个一个执行 |
并行队列 | 当前线程,一个一个执行 | 开很多线程,同时执行 |
2、使用 GCD
都说 GCD 简单易用,那就来用一下:
- 先创建一个队列(或者获取系统的全局队列);
- 将任务追加到队列中。
完了,然后系统就会根据任务类型和队列来执行任务(到底是同步执行,还是异步执行,在那个队列执行)。
2.1 创建队列
主队列(Main Dispatch Queue)
主队列,一个特殊的 串行队列
。所有放到主队列的任务都会放到主线程执行。主要用于刷新 UI,当然,你也可以把任何操作都放到主队列中。
原则上来说任何刷新 UI 的操作都应该放到主队列执行,而耗时操作尽量放到其他线程执行。
主队列无法创建,只能获取。
/// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
全局队列(Global Dispatch Queue)
全局队列,苹果提供给开发者可以直接使用的全局并发队列。
一些与 UI 无关的操作应该放到全局队列来执行,而不是主队列。比如网络请求这类操作。
通过 GCD 提供的 dispatch_get_global_queue
方法获取全局队列:
/*
* @function: dispatch_get_global_queue
* @abstract: 获取全局队列
* @para identifier
队列优先级,一般使用 DISPATCH_QUEUE_PRIORITY_DEFAULT。
* @para flags
保留参数,传 0。传递除零以外的任何值都可能导致返回值为 NULL。
* @result: 返回指定的队列,若失败则返回 NULL
*/
dispatch_queue_global_t
dispatch_get_global_queue(long identifier, unsigned long flags);
dispatch_get_global_queue
第一个参数 identifier
有如下选择:
/*
* - DISPATCH_QUEUE_PRIORITY_HIGH
* - DISPATCH_QUEUE_PRIORITY_DEFAULT
* - DISPATCH_QUEUE_PRIORITY_LOW
* - DISPATCH_QUEUE_PRIORITY_BACKGROUND
*/
/// 派发到此队列的任务将以最高优先级执行
/// 此队列的任务将会被安排到默认优先级及低优先级的任务之前执行
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
/// 派发到此队列的任务将以默认优先级执行
/// 此队列的任务将会被安排在 “所有高优先级任务全部调度完成之后,低优先级任务被调度之前” 调度执行
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
/// 派发到此队列的任务将以低优先级执行
/// 此队列的任务将会被安排在 “所有高优先级和默认优先级的任务全度调度完成之后” 调度执行
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
/// 派发到此队列的任务将以后台优先级执行
/// 此队列的任务将会被安排在所有高优先级任务之后,才会被调度执行。系统将在具有后台状态的线程(setThreadPriority)上运行该队列上的任务,
/// (磁盘 I/O 收到限制,线程的调度优先级被设置为最低值)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
获取 默认优先级 的全局队列
// 获取 默认优先级 的全局队列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
自 iOS 8 开始,苹果还引入了线程的服务质量 qos_class
:
/*
* 苹果建议我们使用服务质量类别的值来标记全局并发队列
* - QOS_CLASS_USER_INTERACTIVE
* - QOS_CLASS_USER_INITIATED
* - QOS_CLASS_DEFAULT
* - QOS_CLASS_UTILITY
* - QOS_CLASS_BACKGROUND
*
* 全局并发队列仍然可以通过优先级来识别,它会被映射到以下QOS类:
* - DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
* - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
* - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
* - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
*/
/*
* @constant QOS_CLASS_USER_INTERACTIVE
* @abstract 这个 QOS 类表明该线程执行与用户交互的工作。
* @discussion 与系统的其他工作相比,这些工作被要求以最高优先级运行。
* 指定这个 QOS 类将会请求几乎所有可以用的系统 CPU 资源和 I/O 带宽运行,甚至不惜争夺资源。
* 这不是一个适合大型任务的节能 QOS 类。这个类应该仅限于与用户关键交互。
* 例如处理主循环的事件,绘图,动画等。
*
* @constant QOS_CLASS_USER_INITIATED
* @abstract 这个 QOS 类表明该线程执行用户发起并可能在等待结果的工作。
* @discussion 这类工作的优先级低于用户关键交互,但又高于系统上的其他操作。
* 这不是一个适合大型任务的节能 QOS 类。它的使用应该被限制在极短的时间内,从而用户不至于在等待期间切换任务。
* 典型的用户发起的通过显示占位符或模态展示用户界面来指示进度的工作。
*
*
* @constant QOS_CLASS_DEFAULT
* @abstrct 系统在缺少具体 QOS 类信息的情况下使用的默认 QOS 类。
* @discussion 这类工作优先级低于用户关键操作和用户发起的工作,但高于实用工具和后台任务。
* 通过 pthread_create 创建且没有指定 QOS 类属性的线程将默认为 QOS_CLASS_DEFAULT。
* 这个 QOS 类并不打算作为工作分类,它应该只作为系统提供给传播或恢复的 QOS 类的值。
*
*
* @constant QOS_CLASS_UTILITY
* @abstract 这个 QOS 类表明该线程执行的工作可能不由用户发起,且用户不期待立即等待结果。
* @discussion 这类工作优先级低于用户关键操作和用户发起的工作,但高于低级别的系统维护工作。
* 这个 QOS 类指明这类工作应该以节能高效方式运行。
* 这种实用工具的工作可能不表明给用户,但是这类工作的影响是用户可见的。
*
*
* @constant QOS_CLASS_BACKGROUND
* @abstract 这个 QOS 类表明该线程执行的工作不由用户发起,且用户可能并不知道结果。
* @discussion 这类工作优先级低于其他工作。
* 这个 QOS 类指明这类工作应该以最节能高效的方式运行。
*
*
* @constant QOS_CLASS_UNSPECIFIED
* @abstract 这是一个指示 QOS 类信息缺失或者被移除的标记。
* @discussion 作为 API 返回值,可能表示线程或 pthread 被不兼容的遗留 API 配置,或与 QOS 类系统冲突。
*/
__QOS_ENUM(qos_class, unsigned int,
QOS_CLASS_USER_INTERACTIVE = 0x21,
QOS_CLASS_USER_INITIATED = 0x19,
QOS_CLASS_DEFAULT = 0x15,
QOS_CLASS_UTILITY = 0x11,
QOS_CLASS_BACKGROUND = 0x09,
QOS_CLASS_UNSPECIFIED = 0x00,
);
面试资料:
面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
自定义队列
苹果允许开发者创建自定义队列,串行队列和并行队列都可以创建。
/*
* @funcion: dispatch_queue_create
* @abstract: 创建自定义队列
* @para label
队列标签,可以为 NULL。
* @para attr
队列类型,串行队列还是并行队列,DISPATCH_QUEUE_SERIAL 与 NULL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并行队列。
* @result: 返回创建好的队列。
*/
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
如注释所说
-
第一个参数为队列标签,可为 NULL,开发者可是用这个值来方便地 DEBUG。队列标签推荐使用应用程序 ID 这种的逆序域名。
-
第二个参数就比较重要了,它表明开发者需要创建的队列类型,串行队列还是并行队列。
串行队列:
DISPATCH_QUEUE_SERIAL
或NULL
。 并行队列:DISPATCH_QUEUE_CONCURRENT
。
2.2 创建任务
搞了这么半天其实都是准备工作,只是为了创建一个可以存放任务的容器。只不过这个容器不可或缺。
/*
* @function: dispatch_sync
* @abstract: 在当前线程同步执行任务,会阻塞当前线程直到这个任务完成。
* @para queue
队列。开发者可以指定在哪个队列执行这个任务
* @para block
任务。开发者在这个 Block 内执行具体任务。
* @result: 无返回值
*/
//void
//dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"同步执行");
});
/*
* @function: dispatch_async
* @abstract: 另开线程异步执行任务,不会阻塞当前线程。
* @para queue
队列。开发者可以指定在哪个队列执行这个任务
* @para block
任务。开发者在这个 Block 内执行具体任务。
* @result: 无返回值
*/
// void
// dispatch_async(dispatch_queue_t queue,
DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"异步执行");
});
如注释所说,第一个参数表明放在哪个队列执行,第二个表明任务是什么。
同步与异步最大的区别就在于是否会阻塞当前线程:
-
dispatch_sync
会阻塞当前线程。 -
dispatch_async
不会阻塞当前线程。
2.3 组合任务与队列
在当前线程为主线程的情况下,任务执行方式与队列种类两两组合一下:
- 同步执行 + 串行队列
- 同步执行 + 并行队列
- 异步执行 + 串行队列
- 异步执行 + 串行队列
对了,还忘了两个,主队列
- 同步执行 + 主队列
- 异步执行 + 主队列
在当前线程为主线程的情况下:
串行队列 | 并行队列 | 主队列 | |
---|---|---|---|
同步执行 | 不开启新线程,串行执行任务 | 不开启新线程,串行执行任务 | 死锁 |
异步执行 | 开启一条新线程,串行执行任务 | 开启新线程(可能会有多条),并发执行任务 | 不开启新线程,串行执行任务 |
说人话:
-
同步执行,在当前线程执行指定任务,而且会阻塞当前线程的后续任务;
在同步执行条件下,并行队列也无法并行,毕竟阻塞了。
注意:同步执行 + 主队列 = 死锁 。
-
异步执行不需要阻塞,开启新线程执行任务,且不阻塞当前线程的后续任务。
在异步执行条件下,串行队列与并行队列都会开启新线程
只不过串行队列值开启一条新线程,并行队列会尽量开启多的线程来分别执行任务(毕竟有上限,不可能同时开启1000000条)。
-
主队列是个串行队列,且只能选择异步执行,毕竟 同步执行 + 主队列 = 死锁 。
验证一下 同步执行 + 串行队列 = 死锁
/**
* 同步执行 + 主队列
*/
- (void)syncMain {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"syncMain --- 开始");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"同步执行 + 主队列");
});
NSLog(@"syncMain --- 结束");
}
程序崩溃了。仅打印出 “当前线程信息” 以及 “syncMain --- 开始”,然后便没有然后了。
先收集当前执行环境:
- 主线程执行;
- 同步执行;
- 主队列任务、
我在上边说过,任何一句代码都是一个任务。
很明显,程序崩溃的时候,正在执行 dispatch_sync
任务(称之为 任务1),而 任务1 的内容是 停止当前工作,立即执行 ^{ NSLog(@"同步执行 + 主队列"); }
(称之为 任务2)。
dispatch_sync
会阻塞当前线程,具体是 【在执行完 Block 之前,dispatch_sync
不会 return】。这就意味着 直到完成 任务2, dispatch_sync
才能 return。
说人话,主线程一直在执行 任务1,除非 任务2 完成。
但是我们是使用 主队列 来执行这里同步操作的,主队列如果要执行下一个任务,那么当前任务必须完成。
此时,任务1 等待任务完成,自己才能完成;而 任务2 等待 任务1 完成,自己才能开始执行。
是不是跟死锁的机制一模一样:我在等着你,而你也在等着我。
没错!!!这里就是死锁,不过新版 GCD 加入了死锁检测机制,如果发生死锁,则会引发 crash。
有兴趣的朋友可以去 Apple 开源代码 - libdispatch 或 GCD源码吐血分析(2)
事实上,并不是 同步执行 + 主队列 = 死锁,而是 在主线程环境下 + 同步执行 + 主队列 = 死锁。
在上升一层, 一个串行队列的任务正在被执行,若此时给这条串行队列同步提交任务时,则会引发 crash。
证明一下:
至于 GCD源码吐血分析(2) 里说的 可以绕开 crash 来引发死锁,我想我可能做到了。。。(感兴趣的朋友可以试试下面这段代码)
- (void)theDeadLock {
NSLog(@"当前线程:%@", [NSThread currentThread]);
dispatch_queue_t theQueue = dispatch_queue_create("com.junes.serialQueue", DISPATCH_QUEUE_SERIAL);
/// 如果是在 主线程中执行,那这里一定要异步,否则 第二层直接凉凉
/// 如果在其他线程,那么这里同步异步没有关系
dispatch_async(theQueue, ^{ /// 第一层
NSLog(@"1 %@", [NSThread currentThread]);
/// 这里一定要 同步执行
/// 如果这里提前 return了,那么 theQueue 中将暂时没有任务
/// 即可以立即执行 第三层任务,就不符合死锁条件
dispatch_sync(dispatch_get_main_queue(), ^{ /// 第二层
NSLog(@"2 %@", [NSThread currentThread]);
NSLog(@"奥利给 %@", [NSThread currentThread]);
});
NSLog(@"3 %@", [NSThread currentThread]);
});
NSLog(@"4 %@", [NSThread currentThread]);
});
NSLog(@"5 %@", [NSThread currentThread]);
}
-
这里触发死锁的原理:执行第三层时,theQueue 必须不能为空。
第二层不能在 第三层完成之前 return,否在 theQueue 中没有任务,那完全可以立即执行 第三层的任务。
-
能避开 crash 的原理:我也不清楚。。。。
反正把 第二层的主队列 换成 全局并行队列或者自定义串行队列都会直接引发 crash。。。
如果有大佬知道原理,求告知。。。拜谢
至于在 队列中嵌套队列 ,这里也给一个表格(【】代表外层操作,并且所有外层操作都能正常运行前提下):
【同步执行 + 串行队列】嵌套同一个串行队列 | 【同步执行 + 并行队列】嵌套同一个并行队列 | 【异步执行 + 串行队列】嵌套同一个串行队列 | 【异步执行 + 并行队列】嵌套同一个并行队列 | |
---|---|---|---|---|
同步 | 死锁 | 当前线程串行执行 | 死锁 | 当前线程串行执行 |
异步 | 另开线程(1条)串行执行 | 另开线程并行执行 | 另开线程(1条)串行执行 | 另开线程并行执行 |
2.4 延迟执行 dispatch_after
需求:延后一段时间再执行任务。
-
dispatch_after
方法可以实现延时执行任务。
其参数为:
- when:再过多久将任务提交至队列;
- queue:提交到哪个队列;
- block:提交什么任务。
dispatch_after
比 NSTimer
优秀,因为他不需要指定 Runloop 的运行模式。 dispatch_after
比 NSObject.performSelector:withObject:afterDelay:
优秀,因为它不需要 Runloop 支持。
NSLog(@"开始执行 dispatch_after");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"三秒后");
});
但是请注意,dispatch_after
并不是在指定时间后执行任务,而是在指定时间之后才将任务提交到队列中。
所以,这个延迟的时间是不精确的。这是缺点之一。
第二个缺点便是,dispatch_after
延后执行的 Block 无法直接取消。但是 Dispatch-Cancel 提供了一种解决方案。
其具体实现可以在 Apple 开源代码 - libdispatchd > Dispatch Source > source.c 中查看,这里就不细说了:
判断 when,如果是现在,异步执行它;否则就创建一个 dispatch source
以便在指定时间触发 dispatch_async
。
2.5 单次执行 dispatch_once
需求:单例模式。
-
dispatch_once
允许开发者在线程安全地执行且只执行一次指定任务。这非常适合单例模式.
其参数为:
- predicate:单次执行的标记;
- block:需要单次执行的任务。
static TheClass *instance = nil;
+ (instance)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[TheClass alloc] init];
});
return instance;
}
GCD 是线程安全的。任何试图访问临界区(即传递给 dispatch_once
的任务)的线程会在临界区已有一个线程的情况下被阻塞,直到临界区线程完成操作。
遗憾的是,Swift 取消了 dispatch_once 这个操作,毕竟在 Swift 中实现单例实在是太简单了(只需要将初始化方法设置为私有,然后提供一个静态实例变量即可)。
这里提供一个 Swift 版的 dispatch_once
:
// MARK: - DispatchQueue once
extension DispatchQueue {
private static var _onceTracker = [String]()
/**
Executes a block code, associated with a unique token, only once.
The clode is thread safe and will only execute the code once even in
the prescence of multithread calls.
- parameter token: A unique reverse DNS style name suce as com.vectorfrom.<name> or a GUID
- parameter block: Block to execute once
**/
public class func once(token: String, block: () -> Void) {
objc_sync_enter(self)
defer {
objc_sync_exit(self)
}
guard !_onceTracker.contains(token) else { return }
_onceTracker.append(token)
block()
}
}
2.6 并发迭代 dispatch_apply
需求: 遍历一个很大很大的集合,使用 for 循环将会花费很多很多事件。
-
dispatch_apply
按照指定的次数将指定的任务提交到指定的队列中,同步执行并等待所有任务完成后 return。
说人话,dispatch_apply
就是一个高级一些的 for 循环,它支持并发迭代。并且它 是同步执行的,必须等到所有工作完成才能返回,这与 for 循环一样。
其参数为:
- iterations:需要迭代的次数;
- queue:将迭代任务提交到哪个队列;
- block:具体的迭代任务是什么。
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"dispatch_apply --- %zu --- %@", index, [NSThread currentThread]);
});
上边的例子并不值得使用 dispatch_apply
:创建并运行线程是需要付出代价的(时间开销,内存开销)。
针对简单的迭代,使用 for 循环远比 dispatch_apply
实惠。如果需要迭代非常大的集合,才应该考虑使用 dispatch_apply
。
dispatch_apply
在各队列上的表现(当前为主线程):
- 主队列:死锁(毕竟这是同步执行);
- 串行队列:串行队列会完全抵消
dispatch_apply
并行迭代的功能,还不如 for 循环; - 并行队列:并行执行迭代任务,这是非常好的选择,也是
dispatch_apply
的意义所在。
2.7 栅栏方法 dispatch_barrier_async
需求:异步执行两组任务,但第二组任务需要第一组完成之后才能执行。
-
dispatch_barrier_async
可以提供一个 “栅栏” 将两组异步执行的任务分隔开,保证先于栅栏方法提交到队列的任务全部执行完成之后,然后开始执行将栅栏任务,等到栅栏任务执行完成后,该队列便恢复原本执行状态。
其参数为:
- queue:需要隔开的任务所在的队列;
- block:栅栏任务的具体内容。
- (void)barrier_display {
NSLog(@"当前线程 -- %@", [NSThread currentThread]);
dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t theQueue = theConcurrentQueue;
dispatch_async(theQueue, ^{
NSLog(@"任务1 开始");
// 模拟耗时任务
[NSThread sleepForTimeInterval:2];
NSLog(@"任务1 完成");
});
dispatch_async(theQueue, ^{
NSLog(@"任务2 开始");
// 模拟耗时任务
[NSThread sleepForTimeInterval:1];
NSLog(@"任务2 完成");
});
dispatch_barrier_async(theQueue, ^{
NSLog(@"================== 栅栏任务 ==================");
});
dispatch_async(theQueue, ^{
NSLog(@"任务3 开始");
// 模拟耗时任务
[NSThread sleepForTimeInterval:4];
NSLog(@"任务3 完成");
});
dispatch_async(theQueue, ^{
NSLog(@"任务4 开始");
// 模拟耗时任务
[NSThread sleepForTimeInterval:3];
NSLog(@"任务4 完成");
});
}
看看 dispatch_barrier_async
与 dispatch_async
查看源码:
-
dispatch_barrier_async
void dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER; dispatch_qos_t qos; qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags); _dispatch_continuation_async(dq, dc, qos, dc_flags); }
-
dispatch_async
void dispatch_async(dispatch_queue_t dq, dispatch_block_t work) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DC_FLAG_CONSUME; dispatch_qos_t qos; qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags); _dispatch_continuation_async(dq, dc, qos, dc->dc_flags); }
可以发现,dispatch_barrier_async
与 dispatch_async
机会一模一样,唯一的区别就在于
/// 这是 dispatch_barrier_async
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
/// 这是 dispatch_async
uintptr_t dc_flags = DC_FLAG_CONSUME;
两者唯一的区别在于 创建 dispatch_qos_t qos
传入的 dc_flags
。
dispatch_barrier_async
比 dispatch_async
多了一个标记 DC_FLAG_BARRIER
。
而这个标记对全局并发队列不起作用。。。。
看看dispatch_barrier_sync
与 dispatch_sync
至于 dispatch_barrier_sync
与 dispatch_sync
。查看源码:
-
dispatch_barrier_sync
:void dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work) { uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK; if (unlikely(_dispatch_block_has_private_data(work))) { return _dispatch_sync_block_with_privdata(dq, work, dc_flags); } _dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags); } static void _dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags) { _dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags); }
-
dispatch_sync
:void dispatch_sync(dispatch_queue_t dq, dispatch_block_t work) { uintptr_t dc_flags = DC_FLAG_BLOCK; if (unlikely(_dispatch_block_has_private_data(work))) { return _dispatch_sync_block_with_privdata(dq, work, dc_flags); } _dispatch_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags); } static void _dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags) { _dispatch_sync_f_inline(dq, ctxt, func, dc_flags); } static inline void _dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags) { if (likely(dq->dq_width == 1)) { /// 串行队列执行到这里 return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags); } if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) { DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync"); } dispatch_lane_t dl = upcast(dq)._dl; // Global concurrent queues and queues bound to non-dispatch threads // always fall into the slow case, see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE /// 通过下面的堆栈,当创建全局并行队列的时候,才会执行到此方法 if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) { return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags); } if (unlikely(dq->do_targetq->do_targetq)) { return _dispatch_sync_recurse(dl, ctxt, func, dc_flags); } _dispatch_introspection_sync_begin(dl); /// 执行 Block _dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))); }
没看明白???
那就看看堆栈:
-
dispatch_barrier_sync
:dispatch_barrier_sync
↘_dispatch_barrier_sync_f
-
dispatch_sync
:dispatch_sync
↘_dispatch_sync_f
↘_dispatch_sync_f_inline
↘_dispatch_barrier_sync_f
如果是串行队列,最后调用到了同一个方法 _dispatch_barrier_sync_f
栅栏方法在在各队列上的表现(当前为主线程):
主队列 | 自定义串行队列 | 全局并行队列 | 自定义并行队列 | |
---|---|---|---|---|
dispatch_barrier_async |
串行队列毫无意义 | 串行队列毫无意义 | 相当于 dispatch_async ,无法达成栅栏目的 |
在之前和之后的任务之间加一道栅栏,栅栏任务在之前的所有任务完成之后开始执行,完成之后恢复队列原本的工作状态 |
dispatch_barrier_sync |
死锁 | 串行执行任务 |
在当前为主线程环境下,一个个验证(串行队列就没必要验证了):
-
dispatch_barrier_async
+ 自定义并行队列
完美符合需求,不是吗?
**自定义并发队列** 是 `dispatch_barrier_async` 最佳拍档。
-
dispatch_barrier_async
+ 全局并行队列
完全跟 `dispatch_async` 相同,根本无法达成栅栏目的。
全局并发队列,可能也会被系统系统使用,请不要为了栅栏而垄断它。
-
dispatch_barrier_sync
+ 主队列死锁,上边的源码解读能找到原因。
-
dispatch_barrier_sync
+ 自定义串行队列串行队列有啥好加栅栏的。。。况且
dispatch_barrier_sync
还会阻塞线程。 -
dispatch_barrier_sync
+ 全局并行队列与
dispatch_barrier_async
+ 全局并行队列类似,毫无栅栏效果。。。 -
dispatch_barrier_sync
+ 自定义并行队列
也是符合要求的,不是吗?
结论:自定义并行队列 是栅栏方法的好帮手
2.8 调度组 dispatch_group_t
需求:分别异步执行几个耗时任务,然后当几个耗时任务都执行完毕后再回到主线程执行任务。
初步看到这个任务,刚才的栅栏任务也能做到嘛,有必要花时间了解一个新东西吗?且往下看。。。
- Dispatch Group 会在整个组的任务全部完成时通知开发者。这些任务可以使同步的,也可以是异步的,甚至可以再不同队列。
要监控如此分散的任务的执行情况,这会让开发者头疼痛不已。幸好有调度组 dispatch_gourp_t
来帮开发者记下这些不同的任务。
- 创建调度组
/// 创建调度组
dispatch_group_t = dispatch_group_create();
- 将任务放进调度组
创建完调度组之后,需要将任务放进调度组中。有两种方式都可以完成这个工作,但是其侧重点不同:
-
dispatch_group_async
;异步请求。任务自动完成,其内部代码执行完毕即视为任务完成。
网络请求一般也是异步请求。所以只要请求发送完成即视为任务完成,但其实任务并没有真正完成
适合内部任务为同步完成的,比如处理一个非常大的集合,或者计算量很大的任务。
-
dispatch_group_enter
。通知调度组有一个任务开始执行了。任务并不会自动完成,需要我们使用
dispatch_group_leave
来告诉调度组有一个任务完成了。适合内部任务为异步完成的,比如异步的网络请求、文件下载。
但是
dispatch_group_enter
必须与dispatch_group_leave
成对出现,否则可能会出现崩溃。
先来验证一下以上说的适不适合的问题,同时也演示一下用法。
- (void)group_validate {
/// 创建一个调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0, 0);
dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
/// 将任务丢进调度组
dispatch_group_async(group, theGlobalQueue, ^{
NSLog(@"任务1 开始 +++++++");
/// 模拟耗时操作
sleep(2);
NSLog(@"任务1 完成 -----------------");
});
dispatch_group_async(group, theSerialQueue, ^{
NSLog(@"任务2 开始 +++++++");
/// 模拟耗时操作
sleep(4);
NSLog(@"任务2 完成 -----------------");
});
dispatch_group_async(group, theConcurrentQueue, ^{
NSLog(@"任务3 开始 +++++++");
/// 模拟异步网络请求
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务3 现在才真正完成 -----------------");
});
NSLog(@"任务3 现在被 dispatch_group_notify 已经完成了");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任务都完成了。。。");
});
}
结果图的红框部分可以证明刚才的理论。
接下来,将“异步的网络操作” 改用 dispatch_group_enter
来放入调度组再看一下。
- (void)group_display {
/// 创建一个调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0, 0);
dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
/// 将任务丢进调度组
dispatch_group_async(group, theGlobalQueue, ^{
NSLog(@"任务1 开始 +++++++");
/// 模拟耗时操作
sleep(2);
NSLog(@"任务1 完成 -----------------");
});
dispatch_group_async(group, theSerialQueue, ^{
NSLog(@"任务2 开始 +++++++");
/// 模拟耗时操作
sleep(4);
NSLog(@"任务2 完成 -----------------");
});
dispatch_group_enter(group);
/// 模拟异步网络请求
dispatch_async(theConcurrentQueue, ^{
NSLog(@"任务3 开始 +++++++");
sleep(5);
NSLog(@"任务3 完成 -----------------");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任务都完成了。。。");
});
NSLog(@"dispatch_group_notify 为异步执行,并不会阻塞线程。我就是证据");
}
面试资料:
面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
这才是使用 调度组 的正常操作!
- 任务完成
-
dispatch_group_notify
;异步执行。指定的调度组内任务全部完成之后,将 Block 加入到特定队列。
对于
dispatch_group_async
的任务,只要其 Block 代码执行完成即认为任务已完成。(无论其 Block 内是否还有异步请求,这一点在上边已经验证过了)对于
dispatch_group_enter
的任务,必须使用dispatch_group_leave
来通知调度组本任务已经完成。 -
dispatch_group_wait
。同步执行,会阻塞线程。
在所有任务完成(或者超时)之前,该方法会一直阻塞线程。
上边已经演示了 dispatch_group_notify
的使用。接下来看一下 dispatch_group_wait
的用法。
- (void)group_display {
/// 创建一个调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0, 0);
dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
/// 将任务丢进调度组
dispatch_group_async(group, theGlobalQueue, ^{
NSLog(@"任务1 开始 +++++++");
/// 模拟耗时操作
sleep(2);
NSLog(@"任务1 完成 -----------------");
});
dispatch_group_async(group, theSerialQueue, ^{
NSLog(@"任务2 开始 +++++++");
/// 模拟耗时操作
sleep(4);
NSLog(@"任务2 完成 -----------------");
});
dispatch_group_enter(group);
/// 模拟异步网络请求
dispatch_async(theConcurrentQueue, ^{
NSLog(@"任务3 开始 +++++++");
sleep(5);
NSLog(@"任务3 完成 -----------------");
dispatch_group_leave(group);
});
NSLog(@"dispatch_group_wait 即将囚禁线程");
/// 传入指定调度组,与超时时间(DISPATCH_TIME_FOREVER 代表永不超时,DISPATCH_TIME_NOW 代表立马超时,完全搞不懂这个有什么用)。
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"dispatch_group_wait 释放了线程");
// 调度组内所有任务都完成了,该做什么就做什么
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"所有任务完成了");
});
}
提醒一下,dispatch_group_wait
第二个参数指明了何时超时。为了方便,苹果提供了 DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
两个常量。
-
DISPATCH_TIME_FOREVER
:永不超时,如果任务一直无法完成,那么线程将一直阻塞。
如果
dispatch_group_leave
数量少于dispatch_group_enter
,那结果值得期待。 -
DISPATCH_TIME_NOW
:立马超时,没有任何异步有机会完成。。。
2.9 信号量 dispatch_semaphore_t
先看一段代码:
__block int theNumber = 0;
/// 创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务 1 开始了 %@", [NSThread currentThread]);
for (int i = 0; i < 10000000; ++i) {
theNumber++;
}
NSLog(@"任务 1 完成了 %@", [NSThread currentThread]);
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务 2 开始了 %@", [NSThread currentThread]);
for (int i = 0; i < 10000000; ++i) {
theNumber++;
}
NSLog(@"任务 2 完成了 %@", [NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"theNumber = %d", theNumber);
});
GCD 来使用多个线程使用同一个资源的例子。最后的执行结果并不是简单的 循环次数 * 2
(当然,需要循环次数稍微大一点。。。)
多线程编程时,不可避免地会发生多个线程使用同一个资源的情况。如果没有锁机制,那么就失去了程序的正确性。
为了确保 GCD 编程的正确性,使用资源时(主要是修改资源)必须加锁。
信号量(dispatch_semaphore_t
),便是 GCD 的锁机制。意为持有计数的信号,苹果提供了 3 个 API 供开发者使用。
-
dispatch_semaphore_create
;根据传入的初始值创建一个信号量。
不可传入负值。运行过程中,若内部值为负数,则这个值的绝对值便是正在等待资源的线程数。
-
dispatch_semaphore_wait
;信号量 -1。
-1 之后的结果值小于 0 时,线程阻塞,并以 FIFO 的方式等待资源。
-
dispatch_semaphore_signal
。信号量 +1。
+1 之后的结果值大于 0 时,以 FIFO 的方式唤醒等待的线程。
给上边的问题代码加上信号量:
__block int theNumber = 0;
/// 创建信号值为 1 的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
/// 创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
/// 信号值 -1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"任务 1 开始了 %@", [NSThread currentThread]);
for (int i = 0; i < 10000000; ++i) {
theNumber++;
}
NSLog(@"任务 1 完成了 %@", [NSThread currentThread]);
/// 信号值 +1
dispatch_semaphore_signal(semaphore);
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
/// 信号值 -1 (此时信号量为负数了,线程阻塞以等待资源)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"任务 2 开始了 %@", [NSThread currentThread]);
for (int i = 0; i < 10000000; ++i) {
theNumber++;
}
NSLog(@"任务 2 完成了 %@", [NSThread currentThread]);
/// 信号值 +1
dispatch_semaphore_signal(semaphore);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"theNumber = %d", theNumber);
});
问题迎刃而解
2.10 调度资源 dispatch_source
dispatch source 是基础数据类型,协调处理特定的底层系统事件。
dispatch source 有以下特征。
- 配置一个 dispatch source 时,需要指定监测的事件、dispatch quue以及处理事件的 Block。
- 当事件发生时,dispatch source 会将指定的 Block 提交到指定的队列上执行。
- 为了防止事件积压到 dispatch queue,dispatch source 采取了事件合并机制。如果新的是时间在上一个事件处理前到达,新旧事件会被合并。根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。
- dispatch source 提供连续的事件,除非显示取消,dispatch source 会一直保留与 dispatch queue 的关联。
- dispatch source 非常轻量,CPU负荷非常小,几乎不占用资源。它是 BSD 系内核惯有功能 kqueue 的包装,kqueue 是 XUN 内核中发生各种事件时,在应用程序编程执行处理的技术。kqueue 可以称为应用程序处理 XUN 内核中丰盛各种事件的方法中最优秀的一种。
dispatch_source
的种类
/*
*当同一时间,一个事件的的触发频率很高,那么Dispatch Source会将这些响应以ADD的方式进行累积,然后等系统空闲时最终处理。
* 如果触发频率比较零散,那么Dispatch Source会将这些事件分别响应。
*/
DISPATCH_SOURCE_TYPE_DATA_ADD 自定义的事件,变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 自定义的事件,变量OR
DISPATCH_SOURCE_TYPE_DATA_REPLACE 自定义的事件,变量Replace
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存报警
DISPATCH_SOURCE_TYPE_PROC 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_READ IO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL 接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_WRITE IO操作,如对文件的操作、socket操作的写响应
DISPATCH_MACH_SEND_DEAD
使用 dispatch source
所有 dispatch source 种类中,最常用的莫过于 DISPATCH_SOURCE_TYPE_TIMER
了。
/*!
* @abstract:创建指定的 dispatch source
* @param type
* 需要创建的 diapatch source 的种类。必须是其种类常量。
*
* @param handle
* 需要监视的基础系统句柄。此参数由 type 参数中提供的常量确定。传 0 即可。
*
* @param mask
* 标志掩码,指定需要哪些事件。此参数由 type 参数中提供的常量确定。传 0 即可。
*
* @param queue
* 在哪个队列处理事件。
*/
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t _Nullable queue);
dispatch_source_create
创建的 dispatch source 处于挂起状态,需要手动唤醒。
配置 dispatch source
/*!
* @abstract
* 配置这个计时器类型的 dispatch source
*
* @param start
* 何时开始接收事件。更多信息查看 dispatch_time() 和 dispatch_walltime()。
*
* @param interval
* 计时器间隔(纳秒级单位)。使用 DISPATCH_TIME_FOREVER 即代表一次性使用。
*
* @param leeway
* 允许的误差(纳秒级单位)。
*/
void
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway);
/*!
* @abstract
* 为给定 dispatch source 设定事件处理。
*
* @param source
* 需要配置的 dispatch dispatch。
*
* @param handler
* 收到事件时的处理回调。前者为 Block,后者为函数。
*/
void
dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler);
void
dispatch_source_set_event_handler_f(dispatch_source_t source,
dispatch_function_t _Nullable handler);
dispatch_source_set_event_handler
使用 Block 作为回调。而 dispatch_source_set_event_handler_f
则使用函数指针作为回调。
启动、挂起、取消 dispatch source
/// 唤醒指定 dispatch source
void
dispatch_resume(dispatch_object_t object);
/// 挂起指定 dispatch source。
void
dispatch_suspend(dispatch_object_t object);
/// 取消指定 dispatch source
void
dispatch_source_cancel(dispatch_source_t source);
/// 查看指定 dispatch source 是否已经取消。已取消返回零,否则非零。
long
dispatch_source_testcancel(dispatch_source_t source);
/// 取消 dispatch source 后最后一次事件的处理。
void
dispatch_source_set_cancel_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler);
关于以上方法,有几下几点需要解释:
- 新创建的
dispatch source
处于挂起状态,必须手动调用dispatch_resume
才能工作; -
dispatch source
处于挂起状态时,发生的所事件都会被累积。dispatch source
被恢复,但是不会一次性传递所有事件,而是先合并到单一事件中; - 取消
dispatch source
是一个异步操作,调用disaptch_source_cancel
之后,不会再有新的事件被传递,但是正在被处理的事件会被继续处理; - 处理完最后的事件之后,
dispatch source
会执行自己的取消处理器(dispatch_source_set_cancel_handler
)。在取消处理器中,可以执行内存和资源的释放工作; - 一定要在
dispatch source
正常工作的情况下取消它。在挂起状态千万不要调用disaptch_source_cancel
取消dispatch source
。
好,上一个完整实例:
__block int countDown = 6;
/// 创建 计时器类型 的 Dispatch Source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
/// 配置这个timer
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
/// 设置 timer 的事件处理
dispatch_source_set_event_handler(timer, ^{
//定时器触发时执行
if (countDown <= 0) {
dispatch_source_cancel(timer);
NSLog(@"倒计时 结束 ~~~");
}
else {
NSLog(@"倒计时还剩 %d 秒...", countDown);
}
countDown--;
});
/// 启动 timer
dispatch_resume(timer);
三、NSOperation 和 NSOperationQueue
NSOperation
和 NSOperationQueue
是基于 GCD 更高一层的封装,完全面向对象。两者分别对应 GCD 的任务与队列。相比 GCD,NSOperation
和 NSOperationQueue
更加简单易用,代码可读性也更高,但是系统开销会稍微大一点。
借用 大佬的一张思维导图 来说明相关的知识点:
3.1 操作 NSOperation
NSOperation
翻译过来就是 “操作”,对应 GCD 中的任务。
NSOperation
是个抽象类,本身无法直接使用,不过 Apple 为我们准备了两个子类:NSInvocationOperation
和 NSBlockOperation
。当然,我们也可以自定义子类(AFNetworking 中自定义了 一个子类 AFURLConnectionOperation)。
NSOperation
是一次性的,它的任务只能被执行一次,执行完之后不能再次执行。
NSoperation
有三种重要状态:
- isReady:返回 YES 则代表已准备好被执行,否则说明还有一些准备工作还未完成;
- isExecuting:返回 YES 则代表正在被执行;
- isFinished:返回 YES 则代表已完成(被取消
isCancelled
也被认为已完成了)。
启动 NSOperation
有两种方式
-
NSOperation
可以配合NSOperationQueue
使用;只需将
NSOperation
添加到NSOperationQueue
中系统会从
NSOperationQueue
中获取NSOperation
然后添加到一个新线程中执行,这种方式默认 异步执行。 -
NSOperation
也可以独立使用。使用
start
方法开启操作。这种方式默认 同步执行。
如果这个
NSOperation
还没有准备好(isReady 返回 NO),那么会触发异常。
推荐 NSOperation
配合 NSOperationQueue
一起使用。
操作依赖 dependencies
当需要以特定顺序执行 NSOperation
时,依赖 是一个方便的选择。
可以使用 addDependency:
与 removeDepencency
来添加或移除依赖。默认情况下,如果一个 NSOperation
的依赖没有执行完成,那么它绝不会准备好;一旦它的最后一个执行完成,这个 NSOperation
就准备好了。
NSOperation
的依赖规则不会区分依赖操作是否真正完成(被取消也被认为完成)。不过,开发者可以决定当依赖操作被取消或未真正完成时是否继续完成这个 NSOperation
。
完成回调 completionBlock
在 NSOperation
完成之后,会在执行这个 NSOperation
的线程回调这个 completionBlock
。
不过,这里的完成,是真正的完成,cancel
是无法触发的。
completionBlock
是一个属性,通过 setter 直接设置即可。
符合 KVO 的属性
NSOperation
类的部分属性是符合 KVC 和 KVO 的。
-
isCancelled
是否被
cancel
。只读。 -
isAsynchronous
是否是异步执行的。只读。
-
isExecuting
是否正在执行。只读。
-
isFinished
是否已完成。只读。
-
isReady
是否已准备好被执行。只读。
-
dependencies
所有的依赖项。只读。
-
queuePriority
队列优先级。可读可写。
-
completionBlock
完成回调 Block。可读可写。
在子类化 NSOperation
时,如果对上述几个属性提供了自定义实现,务必实现 KVC 和 KVO。同样的,要是新增了一些属性,最好也实现 KVC 与 KVO。
子类 NSBlockOperation
NSBlockOperation
以 Block 形式存储任务,使用非常简单。
- 创建
NSBlockOperation
/// 创建方式一:类方法 blockOperationWithBlock:
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"我是 NSBlockOperation 的任务");
}];
/// 创建方式二:
NSBlockOperation *blockOperation2 = [[NSBlockOperation alloc] init];
一般都是使用第一种方式。毕竟第一种方式直接创建了实例并加入了任务。
- 为
NSBlockOperation
添加任务
如果执行以下 blockOperation2 ,你会发现没有任何反应。这也是合乎情理的,毕竟里边没有任何任务。
我们可以使用 addExecutionBlock:
方法来为 NSBlockOperation
添加任务。
[blockOperation2 addExecutionBlock:^{
NSLog(@"我是 NSBlockOperation 的任务");
}];
可以使用 addExecutionBlock:
来为一个 NSBlockOperation
实例添加任务。不论是通过 addExecutionBlock
添加的任务,还是 blockOperationWithBlock
初始化时传入的任务,都保存在其实例属性 executionBlocks
中。没错,一个 NSBlockOperation
实例可以存在多个任务。
可以看出:直接在当前线程执行,并且会阻塞当前线程。
子类 NSInvocationOperation
创建 NSInvocationOperation
的方式也有两种:
/// 创建方式一:分别传入 target / selector / object
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(theInvocationSelector) object:nil];
、
/// 创建方式二:直接传入 NSInvocation 实例
// Method theMethod = class_getInstanceMethod([self class], @selector(theInvocationSelector));
// const char *theType = method_getTypeEncoding(theMethod);
// NSMethodSignature *theMethodSignature = [NSMethodSignaturesignatureWithObjCTypes:theType];
NSMethodSignature *theMethodSignature = [self methodSignatureForSelector:@selector(theInvocationSelector)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:theMethodSignature];
NSInvocationOperation *invicationOperation2 = [[NSInvocationOperation alloc]
initWithInvocation:invocation];
第一种方式较好理解。第二种方式要传入 NSInvocation
实例,不知道是什么东西的朋友可以将 NSInvocation
理解为可以传多个参数的 performSelector:withObject:
即可(或者看一下 iOS - NSInvocation的使用 和 改进的 performSelector)。
自定义子类
自定义 NSOperation
的子类有蛮多需要注意的点。
先借用 大佬的图 来看一下 NSOperation
几个重要方法的默认实现:
另外,NSOperation
有一个非常重要的概念:状态。这些状态改变时,需要发出 KVO 通知,也用一下 大佬的图:
如果只需要自定义非异步(也就是同步)的 NSOperation
,只需要重写 main
方法就好了。如果还想要重写访问 NSOperation
数据的 getter
与 setter
,那请一定保证这些方法时线程安全的。
默认情况下,
main
方法不做任何事情。在重写该方法时,不要调用[super main]
。同时,
main
方法将自动在一个自动释放池中执行,无所另外创建自动释放池。
但对于异步的 NSOperation
, 那么至少要重写以下方法或属性:
-
以异步的方式启动操作。
一旦启动,更新操作的执行属性
executing
。start
前,必须检查是否已经被cancel
。若已被取消绝对不能调用
[super start]
。 -
返回 YES 即可,最好实现 KVO 通知。
-
线程安全地返回操作的执行状态。
值发生变化时必须发出 KVO 通知。KVO 的 keyPath 为 isExecuting。
-
线程安全地返回操作的完成状态。
值发生变化时必须发出 KVO 通知。
一旦操作被取消,任务也被认为完成了。(操作队列在任务完成之后才会移除该操作)
当然,重写以上属性只是最低要求,实际开发中,我们肯定需要重写更多。
从 Apple 官方文档中的 Maintaining Operation Object States 可以找到各个 KVO 支持的属性的 keyPath:
属性 | KVO 的 keyPath | 备注 |
---|---|---|
ready |
isReady |
一般情况无需重写此属性。但如果 ready 的值由外部因素决定,开发者最好提供自定义实现。取消一个正在等待依赖项完成的 NSOperation ,这些依赖项将被忽略而直接将此属性的值更新为 YES,以表示可正常运行。此时,操作队列将更快将其移除。 |
executing |
isExecuting |
若重写 start 方法,则必须重写该属性。并在其值改变时发出 KVO 通知 |
finished |
isFinished |
若重写 start 方法,则必须重写该属性,并在 NSOperation 完成执行或被取消时将值置为 YES 并发出 KVO 通知 |
cancelled |
isCancelled |
不推荐发出此属性的 KVO 通知,毕竟 cancel 时该 NSOperation 的属性 ready 与 finished 的值也会更改 |
注意
-
自定义
NSOperation
时,务必支持cancel
操作。执行任务的主流程应该周期性地检查
cancelled
属性。如果返回 YES,NSOperation
应该尽快清理并退出。如果重写了
start
方法,那么就必须包括取消操作的早期检查。 -
自行管理属性
executing
与finished
时, 务必在executing
属性值置回 NO 时将finished
属性值置为 YES。即使在开始执行之前被取消,也一定要处理好这些改变。
学学大佬 AFNetworking
当然,这里看的并不是最新版本,而是 AFNetworking 的 2.3.1 版本。
AFNetworking 3.0 之后的版本全面使用
NSURLSession
。NSURLSession
本身异步、且不需要 runloop 的配合。因此 3.0 之后的版本并没有使用NSOperation
。
AFURLConnectionOperation
是个 异步 的 NSOperation
子类。来看一看:
- 启动操作
-start
- (void)start {
/// # 1
[self.lock lock];
/// # 2
if ([self isCancelled]) {
/// # 2.1
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
/// # 3
else if ([self isReady]) {
/// # 3.1
self.state = AFOperationExecutingState;
/// # 3.2
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
/// # 4
[self.lock unlock];
}
-
使用
NSRecursiveLock
(递归锁)加锁,保证线程安全; -
检查
NSOperation
是否已被 cancel;2.1 通过 子线程 执行取消操作。
-
检查
NSOperation
是否已经 ready;3.1 更新状态 state,并发出 KVO 通知。其内部也使用了递归锁;
[图片上传中...(image-3a66e5-1592458065424-0)]
<figcaption></figcaption>
3.2 通过 子线程 开始网络请求。
-
操作完成之后,将
NSRecursiveLock
(递归锁)解锁。
在 start
中可以看出, AFNetworking 通过子线程来执行取消操作与真正的任务,来看一看:
- 专用子线程
+networkRequestThread
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
/// # 2.1
@autoreleasepool {
/// # 2.2
[[NSThread currentThread] setName:@"AFNetworking"];
/// # 2.3
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
/// # 2.4
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
/// # 1
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
/// # 2
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
-
使用
dispatch_once
来创建子线程; -
指定线程入口为
networkRequestThreadEntryPoint
。2.1 在 autoreleasepool 在执行操作,方便管理;
2.2 更改线程名,方便使用;
2.3 重点 创建
NSRunloop
对象,保持线程活跃,同时配合NSURLConnection
执行网络请求;2.4 重点 创建
NSMechPort
对象,实现线程间通信,保证始终在创建的子线程处理逻辑。
看完子线程,再看看一下取消 connection 这个操作(这并不是 -cancel
方法):
- 取消 connection
cancelConnection
- (void)cancelConnection {
/// 收集错误信息,就不放了
/// ...
/// # 1
if (![self isFinished]) {
/// # 2
if (self.connection) {
/// # 2.1
[self.connection cancel];
/// # 2.2
[self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:error];
}
/// # 3
else {
self.error = error;
[self finish];
}
}
}
-
检查是否已经 finished;
-
检查
connection
属性是否存在,若存在;2.1 取消当前网络请求;
2.2 调用
onnection:didFailWithError:
保存错误信息,在内部清理工作并执行终结操作finish
。 -
若
connection
不存在。保存错误信息,然后直接执行终结操作
finish
。
- 终结操作
-finish
- (void)finish {
[self.lock lock];
self.state = AFOperationFinishedState;
[self.lock unlock];
/// ...
}
简单明了,在保证线程安全的同时,利用 -setState:
管理自身状态并发出 KVO 通知。
- 执行任务
-operationDidStart
- (void)operationDidStart {
/// # 1
[self.lock lock];
/// # 2
if (![self isCancelled]) {
/// ...
}
[self.lock unlock];
}
- 这里依然使用
NSRecursiveLock
(递归锁)保证线程安全; - 再次检查是否已被 cancel(完全遵循 Apple 明确的 “定期检查
cancelled
属性。”);
好的,最后看一下 Apple 心心念念的取消操作
- 取消操作
-cancel
- (void)cancel {
/// # 1
[self.lock lock];
/// # 2
if (![self isFinished] && ![self isCancelled]) {
/// # 2.1
[super cancel];
/// # 2.2
if ([self isExecuting]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
}
[self.lock unlock];
}
-
依然使用
NSRecursiveLock
(递归锁)保证线程安全; -
检查自身状态,如果已经
finished
或cancelled
,那也除了解锁也没啥号执行的了。2.1 调用
[super cancel]
;(main
与start
务必不要调用父类方法)2.2 检查是否正在
executing
。如果正在执行,跟-start
中一样取消 connection 的任务。
可以看到, NSOperation
本身是存在 -cancel
方法的。但是这里还需要处理自身任务的 cancelConnection
。
毕竟这是个 NSOperation
,其状态不止取决于业务逻辑,还要与其父类沟通好,于是 AFNetworking 方法重写 ready
、 executing
、 finished
的 getter。
- (BOOL)isReady {
return self.state == AFOperationReadyState && [super isReady];
}
- (BOOL)isExecuting {
return self.state == AFOperationExecutingState;
}
- (BOOL)isFinished {
return self.state == AFOperationFinishedState;
}
面试资料:
面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
面试题资料或者相关学习资料都在群文件中 进群即可下载!
最后,抄两个高端操作
-
qualityOfService
服务质量。表示
NSOperation
在获取系统资源时的优先级,默认为NSQualityOfServiceDefault
。优先级最高为
NSQualityOfServiceUserInteractive
。 -
queuePriority
队列优先级。表示
NSOperation
在操作队列中的相对优先级,默认为NSOperationQueuePriorityNormal
。统一操作队列中,优先级更高的
NSOperation
将会被先执行,当然前提是ready
为 YES。最高优先级为
NSOperationQueuePriorityVeryHigh
。吐槽,这个起名有点上头。。。
3.2 NSOperationQueue
NSOperationQueue
,基于优先级与就绪状态执行 NSOperation
的操作队列。
一旦一个 NSOpertaion
被加入到 NSOperationQueue
中,无法直接移除,除非它报告自己完成了操作,否则一直在操作队列中。
将一个 NSOperation
实例加入到 NSOperationQueue
之后,它的 asynchronous
已经没有任何作用了。此时,NSOperationQueue
只是调用 GCD 来异步执行它。
对于操作队列中 ready
为 YES 的 NSOperation
,操作队列将选择 queuePriority
最大的执行。
- 创建操作队列
NSOperationQueue
一共有两种:主队列、自定义队列。
/// 主队列(其实不能叫创建)
NSOperationQueue *theMainOperationQueue = [NSOperationQueue mainQueue];
/// 自定义队列
所有添加到主队列的 NSOperation
都会放到主线程执行。
添加到自定义队列的 NSOperation
,默认放到子线程并发执行。
- 向
NSOperationQueue
添加操作
存在多种方法可以向 NSOperationQueue
添加操作。
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation1");
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation2");
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation3");
}];
NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation4");
}];
NSBlockOperation *operation5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation5");
}];
/// 添加单个 NSOperation
[theOperationQueue addOperation:operation1];
/// 添加多个 NSOperation
[theOperationQueue addOperations:@[operation2, operation3, operation4, operation5] waitUntilFinished:NO];
/// 便利方法,直接添加 Block 到操作队列中
[theOperationQueue addOperationWithBlock:^{
NSLog(@"addOperationWithBlock");
}];
对于 -addOperation:
与 -addOperations:waitUntilFinished:
而言,一个 NSOperation
一次最多只能在一个操作队列中。如果该 NSOperation
已经在某个队列中,则 此方法将会抛出 NSInvalidArgumentException ;同样的,如果某个 NSOperation
正在被执行,也将抛出这个异常。而且,就算第二次添加使用的是同一个队列,也是会抛出该异常的。
需要提醒的是,-addOperations:waitUntilFinished:
的第二个参数如果传入 YES,那么将阻塞当前线程,直到第一个参数中的 NSOperation
全部 finished
。
最后来个特例:-addBarrierBlock:
。意为添加栅栏方法,具体功效请查看方法 dispatch_barrier_asyn
。
-
NSOperationQueue
控制并发数量
NSOperationQueue
有一个名为 maxConcurrentOperationCount
的属性。这个属性的值用来控制一个操作队列中同时最多可以有多少个 NSOperation
参与并发执行。
maxConcurrentOperationCount
默认为 -1,即不限制。同时 Apple 也推荐我们设置为该值,这个值会使系统根据系统条件来设置最大的值。
- 暂停/恢复操作
suspended
suspended
其实只是一个属性。当我们设置它为 YES 时,此时就将队列暂停了;同时将其设置为 NO 时,此时队列恢复。
这里所谓的暂停,并不是设置之后立马暂停,而是执行当前正在执行的操作之后不继续执行。
- 取消操作
-cancelAllOperations
调用 -cancelAllOperations
可以直接取消队列中的所有操作。就是所有操作。。。
跟 suspended
不同,suspended
暂停之后可以恢复。而这里取消了就是真的取消了。
- 操作同步
waitUntilAllOperationsAreFinished
调用此方法之后,阻塞当前线程,直到队列中的所有任务完成。
3.3 对比 GCD 与 NSOperationQueue
最后,借用 大佬的一张图 来对比一下 GCD 与 NSOperationQueue
:
简单的任务使用 GCD 就好了。
如果需要控制并发数、取消任务、添加依赖关系等,那就使用 NSOperation Queue 好了。只不过很多时候都需要子类化 NSOperation。。。
四、iOS 中的锁
iOS 中有很多种锁,先摆上 ibireme 大佬在 不再安全的 OSSpinLock 的性能测试图 :
当然,加锁方案是很多的,比如利用 串行队列
、栅栏方法
、调度组
也可以实现加锁目的。但是这里只讨论 真正的锁。
先来了解几个概念 (参考子 维基百科):
-
临界区:一块对公共资源进行访问的代码。就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。
-
自旋锁:线程反复检查锁变量是否可用。在这个过程中,线程一直保持运行,所以是一种
忙碌等待
。自旋锁有效避免了进程上下文的调度开销,这对于线程阻塞时间很短的场合很有效。但是,单核单线程 CPU 不适用于自旋锁。 -
互斥锁:防止两条线程同时对同一公共资源(比如全局变量,这个变量就是互斥量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。等待互斥锁的线程进入休眠,被唤醒时需要进行上下文切换。
-
读写锁:又称为 “
共享-互斥锁
” 与 “多读者-单写者锁
”。用于解决多线程对公共资源的读写问题。读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。 -
条件锁:条件变量。任务需要的某些资源要求不满足时就进入休眠,条件锁锁上。当被分配到资源时,条件锁解开,任务程继续进行。条件变量总是与互斥量一同出现,
-
递归锁:递归锁可以被一个线程多次 lock,且不会造成死锁问题。递归锁会追踪它被 lock 多少次,只有 unlock 次数与 lock 次数平衡才能真正释放这个锁。递归锁适用于递归方法。
-
信号量:一个同步对象,用于保持在 0 至指定最大值之间的一个计数值。线程对该信号量完成一次 wait,计数值 -1;线程对该信号量完成一次 signal(release),计数值 +1。当计数值为 0 或小于 0 时,线程必须等待该信号量知道其数值大于 0。信号量适用于一个仅能同时被有限数量的用户使用的共享资源,是一种无需 忙碌等待 的锁。
先来一个抢火车票的经典场景:
- (void)trainTicket {
self.trainTicketRemainder = 10000;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 1000; ++i) {
[self buyTrainTicket];
sleep(0.1);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 1000; ++i) {
[self buyTrainTicket];
sleep(0.2);
}
});
}
- (void)buyTrainTicket {
if (self.trainTicketRemainder < 1) {
NSLog(@"票量不足...");
return;
}
self.trainTicketRemainder--;
NSLog(@"售票成功,当前余量: %d", self.trainTicketRemainder);
}
按照常理来讲,最后一个打印的 log 中的数量应该是 10000 - 1000 * 2 = 8000 才对。但是这里的数字是 8028。很明显,这违背了程序的正确性。
发生这种问题的主要原因就在于两个不同的线程同时在对一个共享资源(self.trainTicketRemainder)进行修改。为了避免这种问题,我们就需要对线程进行 加锁。
加锁的原理也不难,来一段伪代码:
do {
Acquire lock /// 获得锁
Critical section /// 临界区
Release lock /// 释放锁
Reminder section /// 非临界区
}
对于上述例子,可以将 self.trainTicketRemainder--
这句代码作为临界区。
4.1 OSSpinLock
自旋锁
从上边的图可以看出,这种锁性能最佳,但是它已经不安全了。
简单描述一下这里的不安全:低优先级线程拿到锁时,高优先级会处于 OSSpinLock
的忙等待状态而消耗大量 CPU 时间,这使低优先级线程抢不到 CPU 时间,从而导致低优先级线程无法完成任务并释放锁。更多请移步 不再安全的 OSSpinLock
这种问题被称为 优先级反转。(这里建议先看一下线程服务质量 qos_class
,或者看一下 2.1 创建队列 中全局队列出的的线程优先级)
使用 OSSpinLock
需要 #import <libkern/OSAtomic.h>
/// 初始化 OSSpinLock
/// OS_SPINLOCK_INIT 默认值为 0,在 locked 状态下大于 0,unlocked 状态下也为 0。
OSSpinLock theOSSpinLock = OS_SPINLOCK_INIT;
/// @abstract 上锁
/// @param __lock : OSSpinLock 的地址
OSSpinLockLock(&theOSSpinLock);
/// @abstract 解锁
/// @param __lock : OSSpinLock 的地址
OSSpinLockUnlock(&theOSSpinLock);
/// @abstract 上锁
/// @discussion 尝试加锁,可以加锁理解加锁并返回 YES,否则返回 NO
/// @param __lock :OSSpinLock 的地址
OSSpinLockTry(&theOSSpinLock);
注意 OSSpinLock
自 iOS 10 已被废弃,使用 os_unfair_lock
代替。
列个表:
锁 | 种类 | 备注 |
---|---|---|
OSSpinLock |
自旋锁 | 不安全,iOS 10 已启用 |
os_unfair_lock |
互斥锁 | 替代 OSSpinLock
|
pthread_mutex |
互斥锁 |
PTHREAD_MUTEX_NORMAL 、#import <pthread.h>
|
pthread_mutex (recursive) |
递归锁 |
PTHREAD_MUTEX_RECURSIVE 、#import <pthread.h>
|
pthread_mutex (cond) |
条件锁 |
pthread_cond_t 、 #import <pthread.h>
|
pthread_rwlock |
读写锁 | 读操作重入,写操作互斥 |
@synchronized |
互斥锁 | 性能差,且无法锁住内存地址更改的对象 |
NSLock |
互斥锁 | 封装 pthread_mutex
|
NSRecursiveLock |
递归锁 | 封装 pthread_mutex (recursive)
|
NSCondition |
条件锁 | 封装 pthread_mutex (cond)
|
NSConditionLock |
条件锁 | 可以指定具体条件值 |
4.2 os_unfair_lock
互斥锁
os_unfair_lock
时 Apple 推荐用于取代不安全的 OSSpinLock
,但仅限于 iOS 10 及以上系统。
os_unfair_lock
是一种互斥锁,处于等待的线程不会像自旋锁那样忙等,而是休眠。
使用 os_unfair_lock
需要 #import <os/lock.h>
。
/// 初始化 os_unfair_lock
os_unfair_lock theOs_unfair_lock = OS_UNFAIR_LOCK_INIT;
/// @abstract 上锁
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_lock(&theOs_unfair_lock);
/// @abstract 解锁
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_unlock(&theOs_unfair_lock);
/// @abstract 上锁
/// @discussion 尝试加锁,可以加锁理解加锁并返回 YES,否则返回 NO
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_trylock(&theOs_unfair_lock);
4.3 pthread_mutex
互斥锁
pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API。
pthread_mutex
可以是一个互斥锁。
使用 pthread_mutex
需要 #import <pthread.h>
/// 定义一个属性变量
pthread_mutexattr_t attr;
/// @abstract 初始化属性
/// @param attr : 属性的地址
pthread_mutexattr_init(&attr);
/// @abstract 设置属性类型为 PTHREAD_MUTEX_NORMAL
/// @param __lock : 属性 pthread_mutexattr_t 的地址
/// @param type : 锁的类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
/// 定义一个锁变量
pthread_mutex_t mutex;
/// @abstract 使用指定属性初始化锁
/// @param mutex : 锁的地址
/// @param attr : 属性 pthread_mutexattr_t 的地址
pthread_mutex_(&mutex, &attr);
/// @abstract 销毁属性
/// @param attr : 属性的地址
pthread_mutexattr_destroy(&attr);
/// @abstract 上锁
/// @param mutex : 锁的地址
pthread_mutex_lock(&mutex);
/// @abstract 解锁
/// @param mutex : 锁的地址
pthread_mutex_unlock(&mutex);
/// @abstract 销毁 pthread_mutex
/// @discussion 一般在 dealloc 中执行
/// @param mutex : 锁的地址
pthread_mutex_destroy(&mutex);
其中,在 pthread_mutexattr_settype
方法的第二个参数代表锁的类型,一共有四种:
#define PTHREAD_MUTEX_NORMAL 0 /// 普通的锁
#define PTHREAD_MUTEX_ERRORCHECK 1 /// 错误检查
#define PTHREAD_MUTEX_RECURSIVE 2 /// 递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL /// 默认的锁,也就是 PTHREAD_MUTEX_NORMAL
当类型是 PTHREAD_MUTEX_DEFAULT
时,相当于 null
。所以上边可以改写为:
pthread_mutexattr_settype(&attr, null);
4.4 pthread_mutex ( recursive )
递归锁
在上一节中,说到 pthread_mutexattr_settype
方法的第二个参数有多种取值。如果这个值传入 PTHREAD_MUTEX_RECURSIVE
,由设置此值属性初始化的 pthread_mutex
就是一个递归锁。
如果是互斥锁或者互斥锁,一个线程对同一个锁加锁多次,那么定会造成思索。但是递归锁允许一个线程对同一个锁多次加锁,不会造成死锁问题。不过,只有 unlock 次数与 lock 次数平衡时,递归锁才会真正释放。
使用 pthread_mutex
需要 #import <pthread.h>
相关方法演示这里就不贴了,跟上一节几乎一模一样,除了这一句 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)
不过举一个例子:
- (void)display_PTHREAD_MUTEX_RECURSIVE {
/// 定义一个属性
pthread_mutexattr_t attr;
/// 初始化属性
pthread_mutexattr_init(&attr);
/// 设置锁的类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
/// 初始化锁
pthread_mutex_init(&mutex, &attr);
/// 销毁属性
pthread_mutexattr_destroy(&attr);
[self test_PTHREAD_MUTEX_RECURSIVE];
/// 销毁锁(一般在 dealloc 中)
pthread_mutex_destroy(&mutex);
}
- (void)test_PTHREAD_MUTEX_RECURSIVE {
static int count = 5;
// 第一次进来直接加锁,第二次进来,已经加锁了。还能递归继续加锁
pthread_mutex_lock(&mutex);
NSLog(@"加锁 %@", [NSThread currentThread]);
if (count > 0) {
count--;
[self test_PTHREAD_MUTEX_RECURSIVE];
}
NSLog(@"解锁 %@", [NSThread currentThread]);
pthread_mutex_unlock(&mutex);
}
4.5 pthread_mutex
条件锁
pthread_mutex
除了互斥锁、递归锁,还可以扮演条件锁。
不过, pthread_mutex
想要扮演条件锁,还需要条件变量 pthread_cond_t
的配合。
使用 pthread_mutex
需要 #import <pthread.h>
/// 初始化锁,使用默认属性
pthread_mutex_init(&mutex, NULL);
/// 定义一个条件变量
pthread_cond_t cond;
/// 初始化条件变量
pthread_cond_init(&cond, NULL);
/// 等待条件(进入休眠时,放开 mutex 锁;被唤醒后,对 mutex 重新加锁)
pthread_cond_wait(&cond, &mutex);
/// 唤醒一个正在等待该条件的线程
pthread_cond_signal(&cond);
/// 唤醒所有正在等待该条件的线程
pthread_cond_broadcast(&cond);
/// 销毁条件变量
pthread_cond_destroy(&cond);
/// 销毁 mutex 锁
pthread_mutex_destroy(&mutex);
条件锁的使用场景并不是特别多。这里使用 “生产者 - 消费者”来演示一下。
先定义几个变量:
/// mutex 锁
pthread_mutex_t mutex;
/// 条件变量
pthread_cond_t cond;
/// 用于保存数据
NSMutableArray *shop;
上代码:
- (void)setup_pthread_cond {
/// 初始化锁,使用默认属性
pthread_mutex_init(&mutex, NULL);
/// 初始化条件变量
pthread_cond_init(&cond, NULL);
/// 唤醒所有正在等待该条件的线程
pthread_cond_broadcast(&cond);
shop = [NSMutableArray array];
NSLog(@"请开始你的表演...");
dispatch_queue_t theQueue = dispatch_get_global_queue(0, 0);
dispatch_async(theQueue, ^{
[self produce];
});
dispatch_async(theQueue, ^{
[self buy];
});
}
/// 假装是生产者
- (void)produce {
while (true) {
pthread_mutex_lock(&mutex);
/// 生产需要时间(doge)
sleep(0.1);
if (shop.count > 5) {
NSLog(@"商店满了,不能再生产了");
pthread_cond_wait(&cond, &mutex);
}
/// 将生产的产品丢进商店
[shop addObject:@"fan"];
NSLog(@"生产了一个 fan");
/// 唤醒一个正在等待的线程
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
/// 假装是消费者
- (void)buy {
while (true) {
pthread_mutex_lock(&mutex);
/// shop 内没有存货,买不到
/// 进入等待(进入休眠,放开 _mutex;被唤醒时,会重新对 _mutex 加锁)
if (shop.count < 1) {
NSLog(@"现在买不到, 我等一下吧");
pthread_cond_wait(&cond, &mutex);
}
[shop removeObjectAtIndex:0];
NSLog(@"终于买到了,不容易");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
iOS设计模式之(二)生产者-消费者 提出了使用 条件锁
的场景。
不得不说,生产者 - 消费者
这种模式能很好地解决 【夺命连环 call】。
4.6 pthread_rwlock
读写锁
pthread_rwlock
,对鞋所。又称为 “共享-互斥锁” 与 “多读者-单写者锁”。用于解决多线程对公共资源的读写问题。读操作可并发重入,写操作是互斥的。
使用 pthread_rwlock
需要 #import <pthread.h>
pthread_rwlock_t rwlock;
/// 初始化锁
pthread_rwlock_init(&rwlock, NULL);
/// 读 - 加锁
pthread_rwlock_rdlock(&rwlock);
/// 读 - 尝试加锁
pthread_rwlock_tryrdlock(&rwlock);
/// 写 - 加锁
pthread_rwlock_wrlock(&rwlock);
/// 写 - 尝试加锁
pthread_rwlock_trywrlock(&rwlock);
/// 解锁
pthread_rwlock_unlock(&rwlock);
/// 销毁锁
pthread_rwlock_destroy(&rwlock);
代码演示:
- (void)setup_pthread_rwlock {
// pthread_rwlock_t rwlock;
/// 初始化锁
pthread_rwlock_init(&rwlock, NULL);
dispatch_queue_t theQueue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 3; ++i) {
dispatch_async(theQueue, ^{
[self write];
});
}
for (int i = 0; i < 3; ++i) {
dispatch_async(theQueue, ^{
[self read];
});
}
}
- (void)write {
pthread_rwlock_wrlock(&rwlock);
sleep(3);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&rwlock);
}
- (void)read {
pthread_rwlock_rdlock(&rwlock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&rwlock);
}
4.7 @synchronized
互斥锁(递归锁)
@synchronized
是 iOS 中使用最简单的锁,但是也是性能最差的锁(见第四章开头的图)。
@synchronized
是互斥锁,当然他也是一个递归锁,不然怎么可能嵌套呢?
它需要一个参数,这个参数是我们要锁住的对象。如果不知道要锁住啥,那就选择 self。
简单的用法演示:
- (void)setup_synchronized {
dispatch_queue_t theQueue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 3; ++i) {
dispatch_async(theQueue, ^{
[self display_synchronized];
});
}
}
- (void)display_synchronized {
@synchronized (self) {
sleep(3);
NSLog(@"%@", [NSThread currentThread]);
}
}
注意 @synchronized
无法锁住“被加锁对象”地址更改的情况,具体原因看这里:
原理也很简单。在 objc_sync_enter
利用 id2data
将传入的对象 id 转换为 SyncData
,然后利用 SyncData.mutex->lock()。Clang 将 @synchronized
改写的源码 clang - RewriteObjC.cpp,真正实现 objc_sync_enter
源码在 runtime 中,下载地址 Apple 官网、github 。
另外,建议大家看下这篇文章 关于 @synchronized,这儿比你想知道的还要多 。
4.8 NSLock
互斥锁
NSLock
,互斥锁。由属性为 PTHREAD_MUTEX_NORMAL
的 pthread_mutex
封装而来的。
iOS 中存在一个 NSLocking
协议:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
NSLock
遵循 NSLocking
协议,可以直接使用 - lock
来加锁,使用 - unlock
来解锁。
此外,NSLock
还提供了 - lockBeforeDate:
与 - tryLock
两种便利性方法。
NSLock
用法演示:
/// 初始化一个 NSLock
NSlock *lock = [[NSLock alloc] init];
/// 加锁
[lock lock];
/// 解锁
[lock unlock];
/// 在 10s 内加锁,成功返回 YES,否则返回 NO。
[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
/// 尝试加锁,成功返回 YES,否则返回 NO。
[lock tryLock];
为了方便, NSLock
还提供一个名为 name
的属性。我们可以头通过这个属性来方便开发。
4.9 NSRecursiveLock
递归锁
NSRecursiveLock
,递归锁。由属性为 PTHREAD_MUTEX_RECURSIVE
的 pthread_mutex
封装而来的。
与 NSLock
相同,NSRecursiveLock
遵循了 NSLocking
协议可以直接使用 - lock
来加锁,使用 - unlock
来解锁。
其 API 与 NSLock
一致,使用方法也完全相同。
面试资料:
面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
面试题资料或者相关学习资料都在群文件中 进群即可下载!