iOS的几种多线程方案
GCD
GCD的常用函数
GCD(GCD源码)中有2个用来执行任务的函数。分别是:
- 采用同步的方式执行任务
1 | //queue代表队列,block代表任务 |
具体使用如下:
1 | //获取全局并发队列 |
- 采用异步的方式执行任务
1 | dispatch_async(dispatch_queue_t queue, dispatch_block_t block); |
具体使用,代码如下:
1 | //获取全局并发队列 |
GCD的队列
GCD的队列可以分为两大类型,分别是
- 并发队列(Concurrent Dispatch Queue)
并发队列是指可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)的队列。并发功能只有在异步(dispatch_async)函数下才有效。
- 串行队列(Serial Dispatch Queue)
串行队列是指让任务一个接着一个地执行的队列(一个任务执行完毕后,再执行下一个任务)。
【注意】主队列(dispatch_queue queue = dispatch_get_main_queue();)其实也是串行队列。
同步/异步、并发/串行
(1)同步和异步主要影响的是:能不能开启新的线程。
同步(dispatch_sync):在当前线程中执行任务,不具备开启新线程的能力。
异步(dispatch_async):在新的线程中执行任务,具备开启新线程的能力。使用异步(dispatch_async)方式执行任务时只能说具备了开启新线程的能力,而并不一定开启新线程。比如传入的是主队列,那么就不会开启新的线程。
(2)并发和串行这两种队列类型主要影响的是:任务的执行方式。
并发:多个任务并发(同步)执行
串行:一个任务执行完毕后,再执行下一个任务。
各种队列的执行效果
【注意】手动创建的串行队列指的是不包含主队列(也是串行队列)的串行队列。
由上表可知,只要是同步任务(sync)不管是哪种队列,都不会开启新线程,并且都是以串行方式执行任务;此外,只要是主队列(dispatch_get_main_queue();),不管是同步(sync)还是异步(async)都不会开启新线程,并且都是以串行方式执行任务。
死锁
所谓死锁,通常是指有两个线程T1和T2都卡住了,并且等待对方完成某些操作。T1不能完成是因为它在等待T2执行完成。而T2也不能完成,因为它在等待T1执行完成。也就是二者都处于“等待对方完成”的状态,于是就导致了死锁(DeadLock)。
以下代码会产生死锁吗?
1 | - (void)viewDidLoad { |
以上代码会产生死锁问题。
那么什么情况下会产生死锁呢?
是否产生死锁的判断标准:当使用dispatch_sync往当前串行队列中添加任务时,会卡住当前的串行队列,进而产生死锁。也就是同时满足下面这三个条件会产生死锁:
- (1)使用dispatch_sync函数
- (2)当前队列(同一个队列)
- (3)串行队列
【注意】只要同时满足以上3个条件就一定会产生死锁。所以解锁死锁,只需要打破以上3个条件中的任意一个,就可以解决死锁问题。
队列组的使用
队列组的使用场景:利用GCD实现以下功能——异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3。
代码如下:
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
多线程的安全隐患
- 资源共享。一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源(比如多个线程访问同一个对象、同一个变量、同一个文件),很容易引发数据错乱和数据安全问题。
多线程安全隐患分析示意图:
多线程安全问题的解决方案:使用线程同步技术(同步:协同步调,按预定的先后次序进行)。常用的线程同步技术是:加锁
iOS中的线程同步方案
- (1)OSSpinLock:等待锁的线程会处于忙等(busy-wait)状态,一直占用CPU资源,所以OSSpinLock是自旋锁(是一种高级锁)。目前OSSpinLock已不再安全,可能会出现优先级反转(Priority Inversion)的问题。有可能产生优先级反转的原因:如果等待锁的线程优先级比较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。所以在项目中不推荐使用OSSpinLock。
那么OSSpinLock为什么会产生优先级反转呢?
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这把锁,它会处于SpinLock的busy-wait(忙等)状态从而占用大量CPU资源,此时低优先级线程无法与高优先级线程争夺CPU时间,从而导致任务迟迟完成不了,进而无法释放lock,从而导致优先级反转。
需要先导入头文件 #import <libkern/OSAtomic.h>,用法如下:
1 | //初始化 |
- (2)os_unfair_lock:os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。从底层调用来看,等待os_unfair_lock锁的线程会处于休眠状态,而不是像自旋锁那样忙等,所以os_unfair_lock是互斥锁,是一种low-level lock(低级锁:低级锁的特点是等不到锁的时候就进入休眠)。
使用时需要导入头文件 #import <os/lock.h>,具体用法如下:
1 | //初始化 |
- (3)pthread_mutex:等待锁的线程会处于休眠状态,所以pthread_mutex是“互斥锁”,是一种low-level lock。
使用时需要导入头文件 #import <pthread.h>,具体用法如下:
首先声明一个属性锁:
1 | @property (nonatomic, assign) pthread_mutex_t mutexLock; |
1 | //初始化属性 |
pthread_mutex-递归锁
将pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);的第二个参数设置为PTHREAD_MUTEX_RECURSIVE,表示该锁是递归锁。除此属性值不同之外,其他用法与pthread_mutex默认的普通锁(PTHREAD_MUTEX_NORMAL)完全一致。
递归锁的特点:允许同一条线程对同一把锁进行重复加锁而不会引发死锁。因为只是允许同一条线程重复加锁,所以递归锁依然能够保证线程安全。
pthread_mutex-条件锁
具体用法参考下面的“多线程的线程间依赖”。
- (4)dispatch_semaphore:semaphore叫做“信号量”,信号量的初始值,可以用来控制线程并发访问的最大数量。信号量的初始值为1,表示同时只允许1条线程访问资源,保证线程同步(实现线程同步的一种方法)。
1 | //信号量的初始值 |
- (5)dispatch_queue(DISPATCH_QUEUE_SERIAL):直接使用GCD的串行队列,也是可以实现线程同步的。
1 | #import "Dispatch_Queue_SerialDemo.h" |
- (6)NSLock:是对pthread_mutex普通锁的OC形式的封装。
1 | #import "NSLockDemo.h" |
- (7)NSRecursiveLock:NSRecursiveLock也是对pthread_mutex递归锁OC形式的封装,API跟NSLock基本一致。
- (8)NSCondition:NSCondition条件锁,是对锁mutex和条件cond的OC封装。
NSCondition的应用场景是某条线程,比如线程A执行到某处时发现条件不满足,此时就会在此处等待,并打开这把锁,允许其他线程去执行任务(加锁解锁)。当其他线程执行完任务后,会解锁,并发送一个符合条件的信号,此时条件满足了,线程A就会给这把锁重新加锁,并往下继续执行代码。NSConditionLock主要用在按照一定顺序执行任务的场合。这也使二者的区别。
1 | @interface NSCondition : NSObject <NSLocking> { |
NSCondition遵守了NSLocking协议,使用的时候同样是lock,unlock加解锁。
具体用法,代码如下:
1 | #import "NSConditionDemo.h" |
- (9)NSConditionLock:是对NSCondition的进一步封装,可以设置具体的条件值。
API如下:
1 | @interface NSConditionLock : NSObject <NSLocking> { |
具体用法如下:
1 | @interface NSConditionLockDemo () |
- (10)@synchronized:是对pthread_mutex递归锁的封装,支持递归加锁。其实现源码可以在objc4的objc-sync.mm文件查看。
@synchronized的实现原理是:利用HashMap(哈希表/散列表),将传入的对象作为key,并通过key获取一把与之对应的锁(Value);如果传入的对象相同,那么获取的锁也是相同的;如果传入的对象不同,那么得到的是不同的锁。
1 | #import "SynchronizedDemo.h" |
多线程的线程间依赖
多线程的线程间依赖指的是有时由于业务需要,只有等执行完线程B中的任务才能再执行线程A中的任务,也就是要求线程A和线程B的执行顺序有要求。那么如何实现线程间依赖呢?可以通过”pthread_mutex-条件锁”来实现。
代码如下:
1 | #import "Pthread_mutexDemo3.h" |
GCD的dispatch_semaphore(信号量)实现“控制最大并发量”
代码如下:
1 | #import "SemaphoreDemo.h" |
GCD的dispatch_semaphore(信号量)实现“线程同步”
信号量的初始值为1(也就是dispatch_semaphore_create(1)),表示同时只允许1条线程访问资源,可以实现线程同步(实现线程同步的一种方法)
代码如下:
1 | #import "SemaphoreDemo2.h" |
iOS线程同步方案性能比较
iOS的各种线程同步方案的实际性能在不同系统,不同设备上可能会有些许差异。ibireme曾对各种锁进行了性能测试(性能测试代码链接),大家可以参考。
性能从高到低排序:
- os_unfair_lock(从iOS10开始支持)
- OSSpinLock(不建议使用)
- dispatch_semaphore(推荐使用)
- pthread_mutex(推荐使用)
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
自旋锁、互斥锁比较
1.什么情况下使用自旋锁比较划算?
- 预计线程等待锁的时间很短。
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生。
- CPU资源不紧张
- 多核处理器
2.什么情况下使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作(因为IO操作都占用CPU资源比较大)
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
atomic
nonatomic:非原子性
atomic:原子性。给属性加上atomic修饰,可以保证属性的setter和getter方法都是原子性操作,相当于在getter和setter内部加上了线程同步的锁。但是atomic并不能保证使用属性的过程是线程安全的。iOS实际开发中之所以不适用atomic修饰属性是因为atomic非常消耗CPU资源。
atomic源码实现可以查看objc4的objc-accessors.mm
iOS中的读写安全最佳方案
iOS中的读写,指的是IO操作(文件操作),包括从文件中读取内容和往文件中写入内容。
1 | - (void)viewDidLoad { |
以上读写操作加锁后虽然是安全的,但是同一时间只能一条线程读取或写入,并不能达到“多读单写”的目标,因此并不是最优解。最优解必须满足“多读单写”的要求:
- (1)同一时间,只能有1条线程进行写的操作;
- (2)同一时间,允许有多个线程进行读的操作;
- (3)同一时间,不允许既有写的操作,又有读的操作。
那么iOS中实现“多读单写”的方案有哪些呢?
- (1) pthread_rwlock:读写锁:等待锁的线程会进入休眠状态。
1 | pthread_rwlock_t lock; |
- (2) dispatch_barrier_async:异步栅栏调用:
dispatch_barrier_async函数传入的并发队列必须是自己通过dispatch_queue_create创建的,读操作和写操作要使用同一个并发队列。如果dispatch_barrier_async函数传入的是一个串行队列或者一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果。
具体使用方法:
1 | //初始化并发队列 |
综上所述,使用pthread_rwlock(读写锁)和dispatch_barrier_async(异步栅栏调用)这两种方案都可以解决读写安全问题,实现“多读单写”的功能。
多线程总结
1.简述你对多线程的理解
- 多线程的概念和作用
- 优势。
2.iOS的多线程方案有哪几种?你更倾向于哪一种?
3.用过GCD吗?在项目中具体是如何使用的?
4.GCD的队列类型有哪些?
5.说一下OperationQueue和GCD的区别以及各自的优势
6.线程安全的处理手段有哪些?
7.OC中的锁你了解哪些?使用以上这些锁需要注意哪些问题?自旋锁和互斥锁的异同?
(1)什么情况下使用自旋锁比较划算?
- 预计线程等待锁的时间很短。
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生。
- CPU资源不紧张
- 多核处理器
(2)什么情况下使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作(因为IO操作都占用CPU资源比较大)
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
8.任选C/OC/C++其中一种语言,实现自旋锁和互斥锁?
9.performSelector: withObject: afterDelay:方法的本质是往RunLoop中添加定时器,子线程默认没有启动RunLoop。
GNUstep
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新实现了一遍并将其开源。虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值。GNUstep源码地址
参考链接: