iOS-多线程-锁
多线程需要一种互斥的机制来访问共享资源。
一、 互斥锁
互斥锁的意思是某一时刻只允许一个线程访问某一资源。为了保证这一点,每个想要访问共享资源的线程,需要首先获得一个共享资源的互斥锁,一旦某个线程对共享资源完成了访问,就释放掉这个互斥锁,这样别的线程就有机会获取互斥锁,然后访问该共享资源了。
一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
1. pthread_mutex
由于 pthread_mutex 有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。
如果已经得到锁,再次申请锁,会导致死锁。然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可。
712028-c2d5d99ae4fb9cfc.png
2. NSLock
NSLock只是在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部pthread_mutex
互斥锁的类型不同。通过宏定义,可以简化方法的定义。
NSLock比pthread_mutex略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。
//设置票的数量为5
_tickets = 5;
//创建锁
_mutexLock = [[NSLock alloc] init];
//线程1
dispatch_async(self.concurrentQueue, ^{
[self saleTickets];
});
//线程2
dispatch_async(self.concurrentQueue, ^{
[self saleTickets];
});
- (void)saleTickets
{
while (1) {
[NSThread sleepForTimeInterval:1];
//加锁
[_mutexLock lock];
if (_tickets > 0) {
_tickets--;
NSLog(@"剩余票数= %ld, Thread:%@",_tickets,[NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@",[NSThread currentThread]);
break;
}
//解锁
[_mutexLock unlock];
}
}
性能与使用场景
pthread_mutex是pthread经典的基于互斥量机制的同步锁,特性、性能以及稳定各方面都已被大量项目所验证,也是比较推荐作为常规同步锁首选
二、自旋锁OSSpinLock
上述文章中已经介绍了 OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,一直循环,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
原理,通过一个全局变量和申请锁的原子操作。
然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。
这些非常底层的概念无需完全掌握,我们只要知道上述申请锁的过程,可以用一个原子性操作 test_and_set 来完成。
实际使用:
#import <libkern/OSAtomic.h>
@interface ViewController ()
{
OSSpinLock spinlock;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.number = 10;
spinlock = OS_SPINLOCK_INIT;
}
- (IBAction)test:(id)sender {
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sellTicket];
});
}
}
- (void)sellTicket {
OSSpinLockLock(&spinlock);
if (self.number > 0) {
self.number--;
NSLog(@"%@还剩%ld张票",[NSThread currentThread],self.number);
}
OSSpinLockUnlock(&spinlock);
}
@end
使用场景
如果临界区的执行时间过长,使用自旋锁不是个好主意,自旋锁适合短时间的操作,加锁性能最快,但不能使用不同优先级。
三、信号量
不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
缺点
在时间较短的操作,没有自旋锁高效,会有上下文切换的成本。
优点
效率高。
四、条件锁
1. NSCondition
NSCondition 其实是封装了一个互斥锁和条件变量。NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。它仅仅是控制了线程的执行顺序。
互斥锁提供线程安全,条件变量提供线程阻塞与信号机制。
它的基本用法和NSLock一样,这里说一下NSCondition的特殊用法。
NSCondition提供更高级的用法,方法如下:
- (void)wait; //阻塞当前线程 直到等待唤醒
- (BOOL)waitUntilDate:(NSDate *)limit; //阻塞当前线程到一定时间 之后自动唤醒
- (void)signal; //唤醒一条阻塞线程
- (void)broadcast; //唤醒所有阻塞线程
2. NSConditionLock
借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:
// 简化版代码
- (id) initWithCondition: (NSInteger)value {
if (nil != (self = [super init])) {
_condition = [NSCondition new]
_condition_value = value;
}
return self;
}
它的 lockWhenCondition 方法其实就是消费者方法:
- (void) lockWhenCondition: (NSInteger)value {
[_condition lock];
while (value != _condition_value) {
[_condition wait];
}
}
对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了所有的消费者:
- (void) unlockWithCondition: (NSInteger)value {
_condition_value = value;
[_condition broadcast];
[_condition unlock];
}
具体使用:
//主线程中
NSConditionLock *theLock = [[NSConditionLock alloc] init];
//线程1
dispatch_async(self.concurrentQueue, ^{
for (int i=0;i<=3;i++)
{
[theLock lock];
NSLog(@"thread1:%d",i);
sleep(1);
[theLock unlockWithCondition:i];
}
});
//线程2
dispatch_async(self.concurrentQueue, ^{
[theLock lockWhenCondition:2];
NSLog(@"thread2");
[theLock unlock];
});
五、递归锁NSRecursiveLock
上文已经说过,递归锁也是通过 pthread_mutex_lock
函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。
NSRecursiveLock
与 NSLock
的区别在于内部封装的 pthread_mutex_t
对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE
。
多次调用不会阻塞已获取该锁的线程,不会死锁。
实际使用:
// 实例类person
Person *person = [[Person alloc] init];
// 创建锁对象
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
// 创建递归方法
static void (^testCode)(int);
testCode = ^(int value) {
[theLock tryLock];
if (value > 0)
{
[person personA];
[NSThread sleepForTimeInterval:1];
testCode(value - 1);
}
[theLock unlock];
};
//线程A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
testCode(5);
});
//线程B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[theLock lock];
[person personB];
[theLock unlock];
});
六、@synchronized
这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。
我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象地址哈希值来得到对应的互斥锁。
若是在self对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。
使用场景:创建单例时使用。
综合上述分析与讨论,总结有以下几点原则:
1、总的来看,推荐pthread_mutex作为实际项目的首选方案;
2、对于耗时较大又易冲突的读操作,可以使用读写锁代替pthread_mutex;
3、如果确认仅有set/get的访问操作,可以选用原子操作属性;
4、对于性能要求苛刻,可以考虑使用OSSpinLock,需要确保加锁片段的耗时足够小;
5、条件锁基本上使用面向对象的NSCondition和NSConditionLock即可;
6、@synchronized则适用于低频场景如初始化或者紧急修复使用;
1.自旋锁:OSSpinLock 在ios中已经不是线程安全的了,如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。(效率最高,如果一直等不到锁会较占用cpu资源)
2.信号量:dispatch_semaphore是gcd中通过信号量来实现共享数据的数据安全。(效率第二)
3.互斥锁:pthread_mutex ,nslock ,synchronized都是互斥锁。如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。(synchronized效率最低)
4.递归锁:pthread_mutex(recursive)与NSRecursiveLock , 多次调用不会阻塞已获取该锁的线程。
5.条件锁:nsconditionlock 满足一定的条件的加锁和解锁,可以实现依赖关系。nscondition条件锁,也是通过信号来解锁,主要用来实现生产者消费者模式。
七、我们平时使用的:
1. @synchronized,一般用在创建单例的时候。
2. atomic修饰属性的关键字,它不是绝对安全的。
3. 一般使用NSLock即可,但是如果方法会有递归调用则会死锁 ,这时我们使用递归锁:
4. OSSpinLock自旋锁,轮询的方式,用于轻量级的数据操作+1/-1。
5. 信号量:dispatch_semaphore是gcd中通过信号量来实现共享数据的数据安全。(效率第二)
资料:
[iOS]深入理解并发--锁
深入理解 iOS 开发中的锁
iOS开发-多线程开发之线程安全篇
iOS 中几种常用的锁总结
iOS中的5种锁