首先,抛出几个面试题,相信看完介绍就会明白怎么解答了。
- 你理解的多线程?
- iOS的多线程方案有哪几种?你更倾向于哪一种?
- GCD 的队列类型
- 线程安全的处理手段有哪些?
- OC你了解的锁有哪些?
ios中常见的多线程方案
我们这里主要讲解GCD
GCD中有两个常用的函数,分别为同步和异步
// 同步
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
queue:队列
block:任务
// 异步
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
队列也分为两大类型:并发队列和串行队列
并发队列:
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效
串行队列:让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
这里同步、异步、并发、串行4个术语容易产生混淆,同步和异步主要影响能不能开启新的线程,并发和串行主要影响任务的执行方式
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务
这里有个注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁),比如
- (void)interview01
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?会!
NSLog(@"1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"2");
});
NSLog(@"3");
// dispatch_sync立马在当前线程同步执行任务
}
打印台:死锁,卡在dispatch_sync(queue, ^{,原因是,同步执行会在执行完1后,直接执行任务2,但是主线程本就是串行,要等执行完interview01这个任务后,再去执行其他的,你等我,我等你,死锁
- (void)interview02
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// dispatch_async不要求立马在当前线程同步执行任务
}
打印台: 1、3、2
- (void)interview03
{
// 问题:以下代码是在主线程执行的
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
打印台: 1、5、2 然后卡在dispatch_sync(queue, ^{ // 1 原因:因为异步,所以正常执行1 5 2,到那时在3的时候,因为同步,要直接执行3,但是因为串行队列,4也要直接执行,所以你等我,我等你,死锁
- (void)interview04
{
// 问题:以下代码是在主线程执行的
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
打印台:1、5、2、3、4
- (void)interview05
{
// 问题:以下代码是在主线程执行的
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
打印台:1、5、2、3、4
多线程的安全隐患
多个线程可能会访问同一块资源,很容易引发数据错乱和数据安全问题
解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行) 常见的线程同步技术是:加锁
iOS中常见的线程同步有:OSSpinLock 、 os_unfair_lock 、 pthread_mutex、 dispatch_semaphore 、dispatch_queue(DISPATCH_QUEUE_SERIAL) 、NSLock 、NSRecursiveLock 、NSCondition 、NSConditionLock 和@synchronized
OSSpinLock
OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
需要导入头文件#import <libkern/OSAtomic.h>
os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock ,
从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
需要导入头文件#import <os/lock.h>
pthread_mutex
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
需要导入头文件#import <pthread.h>
还有个递归锁,只是初始化锁的时候的属性改变
条件锁:初始化一样,只是在需要的地方进行休眠,然后在其他地方进行广播,会发送单个信号通知,唤醒
NSLock、NSRecursiveLock
NSLock是对mutex普通锁的封装,
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition
NSCondition是对mutex和cond的封装
NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
dispatch_semaphore
semaphore叫做”信号量”, 信号量的初始值,可以用来控制线程并发访问的最大数量 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
dispatch_queue
直接使用GCD的串行队列,也是可以实现线程同步的
@synchronized
@synchronized是对mutex递归锁的封装
源码查看:objc4中的objc-sync.mm文件
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
iOS线程同步方案性能比较
性能从高到低排序:os_unfair_lock、 OSSpinLock、 dispatch_semaphore 、pthread_mutex 、dispatch_queue(DISPATCH_QUEUE_SERIAL)、 NSLock 、NSCondition 、pthread_mutex(recursive)、 NSRecursiveLock 、NSConditionLock 、@synchronized
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
这里还有一个属性就是atomic,我们平常都是用的nonatomic,atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁