目录:
- GCD
- 加锁方案
一. GCD
- 说一下iOS中多线程的实现方案
① 这些多线程方案的底层都是依赖pthread
② NSThread线程生命周期是程序员管理,GCD和NSOperation是系统自动管理
③ NSThread和NSOperation都是OC的,更加面向对象
④ NSOperation基于CGD,使用更加面向对象
同步、异步、串行、并发的区别
同步(sync):在当前线程中执行任务,不具备开启新线程的能力
异步(async):在新的线程中执行任务,具备开启新线程的能力
串行:一个任务执行完毕后,再执行下一个任务
并发:多个任务并发(同时)执行dispatch_sync和dispatch_async的区别
dispatch_sync同步的方式执行任务,不具备开启线程的能力,它的任务在当前线程执行的。
dispatch_async异步的方式执行任务,具备开启线程的能力,它的任务在子线程执行的。GCD的队列分类
① 串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
② 并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效
dispatch_sync和dispatch_async函数决定是否在当前线程执行,串行队列和并发队列决定任务是串行执行还是并发执行,他们决定的东西互不影响。
- GCD函数和队列的组合
全局并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
死锁 |
异步(async) | 有开启新线程 并发执行任务 |
有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
死锁产生的原因?
首先要明白两个特点:
队列的特点:FIFO (First In First Out),先进先出。
dispatch_sync函数的特点:sync函数要求立马在当前线程同步执行任务。
原因:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。performSelector:withObject:和performSelector:withObject:afterDelay:的区别?
① performSelector:withObject:的内部直接就是objc_msgSend。
② performSelector:withObject:afterDelay:的内部会创建一个RunLoop,再创建一个定时器,再把定时器添加到RunLoop里面,但是并没有运行RunLoop。
③ run方法内部的确使用while循环一直在调用runMode:beforeDate:。iOS常见的延时执行的方式
① 调用NSObject的方法:performSelector:withObject:afterDelay:,该方法在哪个线程调用,那么run就在哪个线程执行,通常是主线程。
② 使用dispatch_after函数,传入一个队列,如果是主队列,那么就在主线程执行,如果是并发队列,那么会新开启一个线程,在子线程中执行。如何用GCD实现,异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3?
使用GCD队列组,dispatch_group_notify(group, queue, ^{}) 函数会在group里面的任务执行完再执行,如果queue是主队列就在主线程执行这个函数的任务,如果queue是全局并发队列,就在子线程异步执行这个函数的任务。
博客地址:GCD
二. 加锁方案
介绍一下iOS中的加锁方案
OSSpinLock 自旋锁
os_unfair_lock
pthread_mutex
dispatch_semaphore 信号量
dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
1. OSSpinLock
- OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。
- 目前已经不再安全,可能会出现优先级反转问题,也就是,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
- 自旋锁就是下面方式②,类似于写了个while循环,一直占用cpu资源。
- 线程阻塞方式有:
① 让线程睡眠,不占用cpu资源
② while循环,一直占用cpu资源
2. os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持,从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁。所以如果忘记解锁了,那么当下一个线程进来的时候发现没有解锁,就会休眠,当所有的线程都进来 ,就会导致所有的线程都会休眠,这种情况称为死锁,就是永远拿不到锁。
3. pthread_mutex
- mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
- pthread_mutex是跨平台的,一般pthread开头的都是跨平台的,iOS、Linux、Windows等都能使用
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 默认的锁
递归调用产生死锁,用递归锁解决
递归锁:允许同一个线程对一把锁进行重复加锁(解锁)自旋锁和互斥锁的区别:
自旋锁的本质是while循环。
互斥锁是通过线程休眠实现的。
这里讲的锁,除了OSSpinLock是自旋锁,其他的都是互斥锁。pthread_mutex – 条件锁的用法
假设需求是,我们有两个线程,一条线程是删除东西,一条线程是添加东西,删东西和添加东西都要加锁,删除东西时候有个条件,就是必须要有东西才进行删除,那么如何实现?
// 初始化条件
pthread_cond_init(&_cond, NULL);
...
//如果数组为0,就先等等,等有东西了再删除
if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}
...
// 信号 唤醒上面那个线程
pthread_cond_signal(&_cond);
应用场景:
线程1依赖线程2,需要线程2做完某件事再回到线程1继续做事情。比如厂家和消费者的关系,消费者要想买东西,必须要厂家先生产东西。
4. NSLock、NSRecursiveLock、NSCondition
NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装
5. NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。
使用场景:
如果子线程有依赖关系(子线程的执行是有顺序的),就可以使用NSConditionLock,设置条件具体的值。
6. dispatch_queue(DISPATCH_QUEUE_SERIAL)
直接使用GCD的串行队列,也是可以实现线程同步的
dispatch_queue_t moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
···
- (void)__drawMoney {
dispatch_sync(moneyQueue, ^{
[super __drawMoney];
});
}
- (void)__saveMoney {
dispatch_sync(moneyQueue, ^{
[super __saveMoney];
});
}
dispatch_sync函数的特点:要求立马在当前线程同步执行任务(当前线程是子线程,在MJBaseDemo里面已经写了)。
举例说明:比如线程4进来卖票,那么这个操作就会被放到串行队列中,等一会线程7又进来卖票,这个操作也会被放到串行队列中,串行队列里面的东西是:线程4的卖票操作 - 线程7的卖票操作 - 线程5的卖票操作。这样线程4卖完,线程7卖,线程7卖完,线程5卖。这样串行队列中的任务是异步的,不会出现多个线程同时访问一个成员变量的问题,这样也能解决线程安全问题。所以说,线程同步问题也不是必须要通过加锁才能实现。
7. dispatch_semaphore
semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
dispatch_semaphore_t ticketSemaphore = dispatch_semaphore_create(1);
//如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
//如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
//第二个参数代表等到啥时候,传入的DISPATCH_TIME_FOREVER,代表一直等
dispatch_semaphore_wait(ticketSemaphore, DISPATCH_TIME_FOREVER);
......
//让信号量的值+1
dispatch_semaphore_signal(ticketSemaphore);
小提示:控制线程并发访问的最大数量也可以用:
NSOperationQueue *queue;
queue.maxConcurrentOperationCount = 5;
8. @synchronized
@synchronized是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
- (void)__drawMoney {
//最简单的一种方式,但是性能比较差,苹果不推荐使用,所以打出来的时候没提示。
//其中()中是拿什么当做一把锁,比如下面是拿当前类对象当做一把锁。
//为什么把类对象当做一把锁?因为类对象只有一个,以后无论什么实例对象调用这个方法,都是类对象作为锁,这样就只有一把锁,才能锁住。
@synchronized([self class]) {
[super __drawMoney];
}
}
- (void)__saveMoney {
@synchronized([self class]) { // objc_sync_enter
[super __saveMoney];
} // objc_sync_exit
}
注意:@synchronized和@synthesize、@dynamic不一样,别弄混淆了,关于@synthesize、@dynamic可参考Runtime3-objc_msgSend底层调用流程的补充内容。
9. 各种锁的总结
OSSpinLock 自旋锁,因为底层是使用while循环进行忙等,不会进行休眠和唤醒,所以是性能比较高的一把锁,但是现在已经不安全,被抛弃。
os_unfair_lock 用于取代不安全的OSSpinLock ,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁。
pthread_mutex mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。
它是跨平台的,当传入的类型是默认的就是默认锁,当传入PTHREAD_MUTEX_RECURSIVE,就是递归锁,还可以通过pthread_cond_wait(&_cond, &_mutex)当做条件锁来使用。
dispatch_semaphore 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
dispatch_queue(DISPATCH_QUEUE_SERIAL) 直接使用GCD的串行队列,也是可以实现线程同步的
NSLock是对mutex普通锁的封装
NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
@synchronized也是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
10. 各种锁性能比较
性能从高到低排序:
os_unfair_lock iOS10开始支持
OSSpinLock 不安全,被抛弃
dispatch_semaphore 如果需要iOS8、9都支持可以使用
pthread_mutex 可以跨平台
dispatch_queue(DISPATCH_QUEUE_SERIAL) 本来GCD效率就很高
NSLock 对mutex普通锁的封装
NSCondition 对mutex和cond的封装
pthread_mutex(recursive) mutex递归锁,递归锁效率本来就低
NSRecursiveLock 对mutex递归锁的封装
NSConditionLock 对NSCondition的封装
@synchronized 对mutex递归锁的封装
一般推荐使用os_unfair_lock、dispatch_semaphore、pthread_mutex。
说一下 OperationQueue 和 GCD 的区别,以及各自的优势有哪些?
什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长
单核处理器
临界区有IO操作,因为IO操作比较占用CPU资源
临界区代码复杂或者循环量大
临界区竞争非常激烈nonatomic和atomic
对于atomic,setter、getter方法内部会有加锁、解锁操作,nonatomic没有。既然atomic是线程安全的,那么为什么开发中我们基本不用呢?
① 太耗性能了,因为setter、getter方法调用次数太频繁了,如果每次都需要加锁、解锁,那手机CPU资源不就被你消耗完了。所以atomic一般在MAC上才使用。
② 而且只有多条线程同时访问同一个对象的属性,才会有线程安全问题。这种情况几乎没有,如果你非要造出来这种情况,比如:多条线程同时访问 p.data ,那你完全可以在外面加锁嘛!iOS多读单写实现方案
显然,如果使用上面讲的加锁方案,那么无论读、写,同一时间只有一条线程在执行,这样效率比较低,实际上读操作可以同时多条线程一起执行的。iOS中多读单写的实现方案有:
① pthread_rwlock_t:读写锁
② dispatch_barrier_async:异步栅栏调用
- (void)read {
//读-加锁
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
//解锁
pthread_rwlock_unlock(&_lock);
}
- (void)write {
//写-加锁
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
//解锁
pthread_rwlock_unlock(&_lock);
}
//读
dispatch_async(self.queue, ^{
[self read];
});
//写
//当有一条线程在执行这个任务的时候,绝不允许queue中有其他线程在执行其他任务(包括上面的read和下面的write)
dispatch_barrier_async(self.queue, ^{
[self write];
});
//注意:这个dispatch_barrier_async函数传入的并发队列必须是自己手动通过dispatch_queue_cretate创建的。
//如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果。
博客地址:
线程安全、OSSpinLock
加锁方案1
加锁方案2