iOS 中锁的应用
锁的应用是为了保证线程安全
多个线程访问同一块资源的时候,很容易引发数据混乱问题
基本概念
- 自旋锁
用于线程同步,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待(所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁)一旦获取了自旋锁,线程会一直保持该锁,直至释放。自旋锁避免了上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的 - 互斥锁
用于多线程中,防止两条线程同时对同意公共资源进行读写的机制。
在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。
两种锁的应用
互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
1 临界区有IO操作
2 临界区代码复杂或者循环量大
3 临界区竞争非常激烈
4 单核处理器
至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。
- 条件锁
就是条件变量,当进程的某些资源不满足时进入休眠。当分配到资源后,条件锁打开 - semaphore 一种更高级的同步机制。互斥锁可以说是semaphore在仅取值0/1时的特例,信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
一、互斥锁
1、 @synchronized :
@synchronized (self) { // 需要锁定的代码 }
@synchronized 的作用是创建一个互斥锁,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。
()内可以是任何的Objective-C对象
- (void)viewDidLoad {
[super viewDidLoad];
[self synchronized];
}
- (void)synchronized {
NSObject * cjobj = [NSObject new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(cjobj){
NSLog(@"线程1开始");
sleep(3);
NSLog(@"线程1结束");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(cjobj){
NSLog(@"线程2");
}
});
}
控制台输出:
2017-10-18 11:35:13.459194+0800 Thread-Lock[24855:431100] 线程1开始
2017-10-18 11:35:16.460210+0800 Thread-Lock[24855:431100] 线程1结束
2017-10-18 11:35:16.460434+0800 Thread-Lock[24855:431101] 线程2
注意:@synchronized(cjobj) 指令使用的 cjobj 为该锁的唯一标识,只有当标识相同时,才为满足互斥,
如果线程 2 中的 @synchronized(cjobj) 改为 @synchronized(self) ,那么线程 2 就不会被阻塞
优点:不需要在代码中显式的创建锁对象,便可以实现锁的机制
缺点:@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。
2、NSLock
NSLock * cjlock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cjlock lock];
NSLog(@"线程1加锁成功");
sleep(2);
[cjlock unlock];
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[cjlock lock];
NSLog(@"线程2加锁成功");
[cjlock unlock];
NSLog(@"线程2解锁成功");
});
2020-03-28 15:56:57.580346+0800 iOSLockDemo[71692:3779253] 线程1加锁成功
2020-03-28 15:56:59.584020+0800 iOSLockDemo[71692:3779253] 线程1解锁成功
2020-03-28 15:56:59.584024+0800 iOSLockDemo[71692:3779258] 线程2加锁成功
2020-03-28 15:56:59.584163+0800 iOSLockDemo[71692:3779258] 线程2解锁成功
tryLock 返回 YES NO
NSLock * cjlock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cjlock lock];
NSLog(@"线程1加锁成功");
sleep(2);
[cjlock unlock];
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([cjlock tryLock]) {
NSLog(@"线程3加锁成功");
[cjlock unlock];
NSLog(@"线程3解锁成功");
}else {
NSLog(@"线程3加锁失败");
}
});
2020-03-28 16:08:23.535844+0800 iOSLockDemo[71997:3790084] 线程1加锁成功
2020-03-28 16:08:23.535844+0800 iOSLockDemo[71997:3790083] 线程3加锁失败
2020-03-28 16:08:25.539981+0800 iOSLockDemo[71997:3790084] 线程1解锁成功
lockBeforeDate
NSLock * cjlock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cjlock lock];
NSLog(@"线程1加锁成功");
sleep(2);
[cjlock unlock];
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([cjlock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
NSLog(@"线程5加锁成功");
[cjlock unlock];
NSLog(@"线程5解锁成功");
}else {
NSLog(@"线程5加锁失败");
}
});
2020-03-28 16:06:39.145099+0800 iOSLockDemo[71974:3788856] 线程1加锁成功
2020-03-28 16:06:41.149201+0800 iOSLockDemo[71974:3788856] 线程1解锁成功
2020-03-28 16:06:41.149202+0800 iOSLockDemo[71974:3788857] 线程5加锁成功
2020-03-28 16:06:41.149957+0800 iOSLockDemo[71974:3788857] 线程5解锁成功
总结
- 除 lock 和 unlock 方法外,NSLock 还提供了 tryLock 和 lockBeforeDate:两个方法
- 由上面的结果可以看到 tryLock 并不会阻塞线程,[cjlock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码
- lockBeforeDate: 方法会在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回 YES
3、thread_mutex 与 pthread_mutex(recursive):互斥锁(C语言)
pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,POSIX 互斥锁是一种超级易用的互斥锁,
使用的时候:
只需要使用 pthread_mutex_init 初始化一个 pthread_mutex_t,
pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定 ,
pthread_mutex_unlock 来解锁,
pthread_mutex_destroy 来销毁锁。
导出#import <pthread.h>
#pragma mark -- pthread_mutex_t
-(void)pthread_mutex_twithMethod
{
__block pthread_mutex_t cjlock;
pthread_mutex_init(&cjlock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&cjlock);
NSLog(@"线程1开始");
sleep(3);
NSLog(@"线程1结束");
pthread_mutex_unlock(&cjlock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
pthread_mutex_lock(&cjlock);
NSLog(@"线程2");
pthread_mutex_unlock(&cjlock);
});
}
2020-03-28 17:07:14.440619+0800 iOSLockDemo[72429:3813295] 线程1开始
2020-03-28 17:07:17.443088+0800 iOSLockDemo[72429:3813295] 线程1结束
2020-03-28 17:07:17.443386+0800 iOSLockDemo[72429:3813294] 线程2
二、自旋锁 OSSpinLock
自选锁,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。
SSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
...
OSSpinLockUnlock(&lock);
上面是OSSpinLock使用方式,编译会报警告,已经废弃了
os_unfair_lock:(互斥锁)
os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。os_unfair_lock是一种互斥锁,它不会向自旋锁那样忙等,而是等待线程会休眠。
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
#import "os_unfair_lockDemo.h"
#import <os/lock.h>
@interface os_unfair_lockDemo()
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end
@implementation os_unfair_lockDemo
- (instancetype)init
{
self = [super init];
if (self) {
self.ticketLock = OS_UNFAIR_LOCK_INIT;
}
return self;
}
//卖票
- (void)sellingTickets{
os_unfair_lock_lock(&_ticketLock);
[super sellingTickets];
os_unfair_lock_unlock(&_ticketLock);
}
@end
三、递归锁 NSRecursiveLock
允许同一个线程对同一把锁进行重复加锁。要考重点同一个线程和同一把锁
-(void)NSRecursiveLockMethod
{
NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(10);
});
}
写法可以和NSLock一模一样,NSLock和NSRecursiveLock的区别
NSLock lock了之后,没有unlock那么会发生死锁。
允许同一线程多次加锁,而不会造成死锁,但是没有及时unlock,是会导致其他线程阻塞的,还是得记得unlock。
四、条件锁
根据一定条件满足后进行 加锁/解锁.
@interface NSConditionLock : NSObject <NSLocking> {
@private
void *_priv;
}
//初始化一个NSConditionLock对象
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition; //锁的条件
//满足条件时加锁
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
//如果接收对象的condition与给定的condition相等,则尝试获取锁,不阻塞线程
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//解锁后,重置锁的条件
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
//在指定时间前尝试获取锁,若成功则返回YES 否则返回NO
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
//主线程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[lock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
}
else {
NSLog(@"线程2尝试加锁失败");
}
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// sleep(2);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[lock unlock];
NSLog(@"线程3解锁成功");
}
else {
NSLog(@"线程3尝试加锁失败");
}
});
//线程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// sleep(3);//以保证让线程2的代码后执行
[lock lockWhenCondition:2];
NSLog(@"线程4");
[lock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
});
打印结果
2020-03-28 17:53:45.896078+0800 iOSLockDemo[72828:3836240] 线程3尝试加锁失败
2020-03-28 17:53:46.900668+0800 iOSLockDemo[72828:3836241] 线程2
2020-03-28 17:53:46.900914+0800 iOSLockDemo[72828:3836241] 线程2解锁成功
2020-03-28 17:53:46.900917+0800 iOSLockDemo[72828:3836242] 线程4
2020-03-28 17:53:46.901014+0800 iOSLockDemo[72828:3836242] 线程4解锁成功
2020-03-28 17:53:46.901031+0800 iOSLockDemo[72828:3836243] 线程1
结果说明:
1 初始化一个条件锁,条件为0
2 由于线程1 和线程4条件不满足,所以循环一段时间休眠,等待满足条件满足时唤醒;线程3尝试加锁,不会阻塞线程,但是条件不满足所以直接休眠;线程2休眠1秒后尝试加锁。满足条件所以加锁成功;
3 线程2伴随重置加锁条件2进行解锁;
4 此时线程4满足条件,系统唤醒进行加锁,并且重置加锁条件14
5 此时线程1满足条件,系统唤醒进行加锁,并且解锁,此时条件为1
各种锁的性能比较
屏幕快照 2020-03-26 下午5.46.34.png注意:
1.这个数字仅仅代表每次加解锁的耗时,并不能全方面的代表性能
2.不同的机型和系统,不同的循环次数可能结果会略微有些差异
但是还是可以看出@synchronized:是表现最差的。
五、死锁
死锁就是队列引起的循环等待
1、一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");// 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");// 任务2
});
NSLog(@"3");// 任务3
}
结果输出1
分析
- dispatch_sync表示是一个同步线程;
- dispatch_get_main_queue表示运行在主线程中的主队列;
- 任务2是同步线程的任务。
首先执行任务1,接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。
但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务,那么,现在任务2就会被加到最后,任务3排在了任务2前面,任务3要等任务2执行完才能执行,任务2由排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
2、同样,下边的代码也会造成死锁:
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});
外面的函数无论是同步还是异步都会造成死锁。
**这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue2, ^{
NSLog(@"deadlock");
});
});
这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。
3、NSLock死锁
-(void)methodA
{
self.lock = [[NSLock alloc]init];
[self.lock lock];
NSLog(@"1");
[self methodB];
NSLog(@"2");
[self.lock unlock];
}
-(void)methodB
{
[self.lock lock];
NSLog(@"3");
[self.lock unlock];
}
2020-09-25 16:40:48.177198+0800 XXX TEST [2678:167611] 1
输出结果:1
原因如下:由于当前线程运行到第一个lock加锁,现在再次运行到lock同样的锁,需等待当前线程解锁,把当前线程挂起,不能解锁
NSLock是非递归锁,当同一线程重复获取同一非递归锁时,就会发生死锁
解决办法:
我们可以用NSRecursiveLock或者@synchronized替代NSLock
因为NSRecursiveLock是递归锁,@synchronized是同步互斥锁
递归锁:它允许同一线程多次加锁,而不会造成死锁。
-(void)methodA
{
self.lock = [[NSLock alloc]init];
[self.lock lock];
NSLog(@"1");
[self methodB];
NSLog(@"2");
[self.lock unlock];
}
-(void)methodB
{
@synchronized (self) {
NSLog(@"3");
}
或
NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
[lock lock];
NSLog(@"3");
[lock unlock];
// [self.lock lock];
// NSLog(@"3");
// [self.lock unlock];
}
信号量Dispatch Semaphore 也是锁
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。
Dispatch Semaphore 提供了三个函数
- dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
- dispatch_semaphore_signal:发送一个信号,让信号总量加1
- dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
1、保持线程同步:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger number = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后当前线程继续执行
2、保证线程安全,为线程加锁:
在线程安全中可以将dispatch_semaphore_wait看作加锁,而dispatch_semaphore_signal看作解锁
首先创建全局变量
_semaphore = dispatch_semaphore_create(1);
注意到这里的初始化信号量是1。
- (void)asyncTask
{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
count++;
sleep(1);
NSLog(@"执行任务:%zd",count);
dispatch_semaphore_signal(_semaphore);
}
异步并发调用asyncTask
for (NSInteger i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self asyncTask];
});
}
然后发现打印是从任务1顺序执行到100,没有发生两个任务同时执行的情况。
原因如下:
在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。
自旋锁优缺点
自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
自旋锁的缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用