iOS 开发常见的几种锁
简介
在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题。我们常常会使用一些锁来保证程序的线程安全,保证每次只有一个线程访问这一块资源。
多线程编程中,应该尽量避免资源在线程之间共享,以减少线程间的相互作用。
锁的分类
锁住要分为以下几类:
互斥锁
它将代码切片成为一个个代码块,使得当一个代码块在运行时,其他线程不能运行他们之中的任意片段,只有等到该片段结束运行后才可以运行。通过这种方式来防止多个线程同时对某一资源进行读写的一种机制。常用的有:
-
@synchronized
-
NSLock
-
pthread_mutex
自旋锁
多线程同步的一种机制,当其检测到资源不可用时,会保持一种“忙等”的状态,直到获取该资源。它的优势在于避免了上下文的切换,非常适合于堵塞时间很短的场合;缺点则是在“忙等”的状态下会不停的检测状态,会占用 cpu
资源。常用的有:
-
OSSpinLock
-
atomic
条件锁
通过一些条件来控制资源的访问,当然条件是会发生变化的。常用的有:
-
NSCondition
-
NSConditionLock
信号量
是一种高级的同步机制。互斥锁可以认为是信号量取值0/1时的特例,可以实现更加复杂的同步。常用的有:
- dispatch_semaphore
递归锁
它允许同一线程多次加锁,而不会造成死锁。递归锁是特殊的互斥锁,主要是用在循环或递归操作中。常用的有:
-
pthread_mutex(recursive)
-
NSRecursiveLock
读写锁
是并发控制的一种同步机制,也称“共享-互斥锁”,也是一种特殊的自旋锁。它把对资源的访问者分为读者和写者,它允许同时有多个读者访问资源,但是只允许有一个写者来访问资源。常用的有:
-
pthread(rwlock)
-
dispatch_barrier_async / dispatch_barrier_sync
常见几种锁的使用方法
OSSpinLock(iOS 10 以后废弃)
它是一种自旋锁,只有加锁,解锁,尝试加锁三个方法,其中尝试加锁是非线程阻塞的。通过导入 #import <libkern/OSAtomic.h>
引入并调用,使用示例:
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
//执行代码
OSSpinLockUnlock(&lock);
OSSpinLock 有可能会造成死锁,不再安全的锁:
有可能在优先级比较低的线程里对共享资源加锁了,然后高优先级的线程抢占了低优先级的调用 cpu
时间,导致高优先级的线程一直在等待低优先级的线程释放锁,然而低优先级的线程根本没法抢占高优先级的 cpu
时间。(优先级反转)
NSLock
是一种互斥锁,在 Cocoa
程序下 所有锁(包括 NSLock
、NSCondition
、NSRecursiveLock
、NSConditionLock
) 的接口实际上是通过 NSLocking
协议定义的,它定义了 lock
和 unlock
方法,使用这些方法来获取和释放该锁。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
此外,NSLock
类还添加了如下方法:
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回 NO
- lockBeforeDate: 方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)
NSRecursiveLock
是一个递归锁。NSRecursiveLock
类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它被多少次 lock
。每次成功的 lock
都必须平衡调用unlock
操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。在使用锁时最容易犯的一个错误就是在递归或循环中造成死锁,如下代码:
//创建锁
NSLock *lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT), ^{
static void(^TestBlock)(int);
TestBlock = ^(int value) {
NSLog(@"加锁: %d",value);
[lock lock];
if (value > 0) {
TestBlock(--value);
}
NSLog(@"程序退出!");
[lock unlock];
};
TestBlock(5);
});
在线程1中的递归 block 中,锁会被多次的 lock
,所以自己也被阻塞了。此处将NSLock
换成 NSRecursiveLock
,便可解决问题。
NSRecursiveLock *rslock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void(^TestBlock)(int);
TestBlock = ^(int value) {
NSLog(@"加锁: %d",value);
[rslock lock];
if (value > 0) {
TestBlock(--value);
}
NSLog(@"程序退出!");
[rslock unlock];
};
TestBlock(5);
});
NSCondition
NSCondition
的对象实际上是作为一个锁和线程检查器,锁主要是为了检测条件时保护数据源,执行条件引发的任务。线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
NSCondition
除了 lock
和 unlock
方法来使用解决线程同步问题,还提供了更高级的用法:
- (void)wait; //让当前线程处于等待状态
- (BOOL)waitUntilDate:(NSDate *)limit; //让当前线程处于等待到什么时间
- (void)signal; // CPU发信号告诉正在等待中的线程不用在等待,可以继续执行(只对一个线程起作用)
- (void)broadcast;// 通知所有在等在等待中的线程(广播)
@property (nullable, copy) NSString *name;
- wait 堵塞当前线程,使线程进入休眠,等待唤醒信号。
- waitUntilDate 堵塞当前线程,线程进入休眠,等待唤醒信号或者超时。如果是被信号唤醒返回
YES
,否者返回NO
。 - signal 唤醒一个正在休眠的线程,如果要唤醒多个线程,需要调用多次,如果没有线程在等待,什么也不做。
- broadcast 唤醒所有在等待的线程,如果没有线程在等待,什么也不做。
以上的方法在调用前必须已经加锁(lock)
NSCondition
是条件,条件是我们自己决定的。和 NSLock
、@synchronized
等是不同的是,NSCondition
可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。代码示例:
NSCondition *condition =[[NSCondition alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[condition lock];
NSLog(@"线程1加锁");
while ([self.testArray count] == 0) {
NSLog(@"waiting...");
[condition wait];
}
[self.testArray removeObjectAtIndex:0];
NSLog(@"delete one object");
NSLog(@"线程1退出");
[condition unlock];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[condition lock];
NSLog(@"线程2加锁");
self.testArray = [NSMutableArray array];
[self.testArray addObject:[[NSObject alloc] init]];
NSLog(@"add one object");
[condition signal];
NSLog(@"线程2退出");
[condition unlock];
});
condition
进入到判断条件中,当 self.testArray count == 0
时,condition
会调用 wait
,当前线程处于等待状态,其他线程开始访问 self.testArray,当对象创建完毕并加入 self.testArray 中时,cpu 会发出 signal 信号,处于等待的线程就会被唤醒,开始执行 [self.testArray removeObjectAtIndex:0];
。
打印结果如下:
NSConditionLock
NSConditionLock
为条件锁,只有 condition
参数与初始化时候的 condition
相等,才能进行加锁操作。而 unlockWithCondition:
并不是当 condition
符合条件时才解锁,而是解锁之后,修改 condition
的值。提供的方法如下:
- (instancetype)initWithCondition:(NSInteger)condition; //初始化对象。有一个整形的conditon参数,表示条件
- (void)lockWhenCondition:(NSInteger)condition; //进程会一直阻塞,一直到满足conditon并完成加锁
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition; //解锁并重新设定condition
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
我们来看个示例:
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:3];
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"线程1开始执行");
[lock unlockWithCondition:0];
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lockWhenCondition:2];
NSLog(@"线程2开始执行");
[lock unlockWithCondition:1];
});
//线程3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
NSLog(@"线程3开始执行");
[lock unlock];
});
//线程4
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lockWhenCondition:3];
NSLog(@"线程4开始执行");
[lock unlockWithCondition:2];
});
//线程5
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
NSLog(@"线程5开始执行");
[lock unlock];
});
分析:
-
线程1,2调用
[NSConditionLock lockWhenCondition:]
,此时因为不满足当前条件,所以会进入等待状态。 -
线程3,5调用
[NSConditionLock lock:]
,不需要比对条件值,按照cpu
执行顺序执行, -
线程4执行
[NSConditionLock lockWhenCondition:]
,因为满足条件值,所以线程4会按照cpu
执行顺序执行。 -
线程4打印完成后会调用
[NSConditionLock unlockWithCondition:]
,这个时候将条件设置为2,并发送boradcast
,此时线程2接收到当前的信号,唤醒执行并打印;之后会执行线程1。
[NSConditionLock lockWhenCondition:]
这里会根据传入的 condition
值和 value
值进行对比,如果不相等,这里就会阻塞。而 [NSConditionLock unlockWithCondition:]
会先更改当前的 value
值,然后调用 boradcast
,唤醒当前的线程。综上所述,上面的打印结果不是一定的,421 的顺序是一定的,而 3,5 是在任意位置(即只要是按照421的结果顺序都是正确的)
NSCondition & NSConditionLock 比较
相同点:
-
都是互斥锁
-
通过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的
不同点:
-
NSCondition
是基于对pthread_mutex
的封装,而NSConditionLock
是对NSCondition
做了一层封装 -
NSCondition
需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程,NSConditionLock
只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程
@synchronized
是一个 OC
层面的互斥锁,主要是通过牺牲性能换来语法上的简洁与可读。(性能较差不推荐使用)
@synchronized
后面需要紧跟一个 OC
对象,它实际上是把这个对象当做锁的唯一标识。这是通过一个哈希表来记录表示,OC
在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值在数组中得到对应的互斥锁。示例如下:
//总票数
_tickets = 5;
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTickets];
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTickets];
});
- (void)saleTickets {
while (1) {
@synchronized (self) {
[NSThread sleepForTimeInterval:1];
if (_tickets > 0) {
_tickets--;
NSLog(@"剩余票数:%ld, Thread:%@", _tickets, [NSThread currentThread]);
}
else {
NSLog(@"票卖完了 Thread:%@", [NSThread currentThread]);
break;
}
}
}
}
注意点:
- 1.加锁的代码尽量少
- 2.添加的OC对象必须在多个线程中都是同一对象
- 3.优点是不需要显式的创建锁对象,便可以实现锁的机制。
- @synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。
dispatch_semaphore
dispatch_semaphore
是 GCD
使用信号量控制并发。
信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
在日常开发中利用 GCD
的信号量机制来处理一些日常功能的时候,主要会用到的方法有三个:
//创建信号量,会根据传入的参数创建对应数目的信号量
dispatch_semaphore_create(intptr_t value);;
//等待信号量,减少信号量计数
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
// 发送信号量,增加信号量计数
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
-
dispatch_semaphore_create 创建信号量,并且创建的时候需要指定信号量的大小
-
dispatch_semaphore_wait 等待信号量,如果信号量为0,那么该函数就会一直等待(不返回,阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。
-
dispatch_semaphore_signal 发送信号量。该函数会对信号量的值进行加1操作
等待信号量和发送信号量的函数是成对出现的。下面来看个经典的示例(异步函数+并发队列实现同步操作):
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务%d:%@", i + 1, [NSThread currentThread]);
// 发送信号量
dispatch_semaphore_signal(semaphore);
});
// 等待信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1000:%@",[NSThread currentThread]);
});
打印结果太长,截取一部分如下:
可以看到:虽然任务是一个接一个被同步(说同步并不准确)执行的,但因为是在并发队列,并不是所有的任务都是在同一个线程执行的(所以说同步并不准确)。有别于异步函数+串行队列的方式(异步函数+ 串行队列的方式中,所有的任务都是在同一个新线程被串行执行的)。
同步和异步决定了是否开启新线程(或者说是否具有开启新线程的能力),串行和并发决定了任务的执行方式——串行执行还是并发执行(或者说开启多少条新线程)
pthread
pthread,可以创建互斥锁、递归锁、读写锁、once等锁
互斥锁
不会忙等,而是阻塞线程并睡眠,需要进行上下文切换。
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, PTHREAD_MUTEX_NORMAL);
/**
PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
*/
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"线程1加锁");
pthread_mutex_lock(&mutex);
sleep(2);
NSLog(@"线程1");
pthread_mutex_unlock(&mutex);
NSLog(@"线程1解锁");
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"线程2加锁");
pthread_mutex_lock(&mutex);
sleep(2);
NSLog(@"线程2");
pthread_mutex_unlock(&mutex);
NSLog(@"线程2解锁");
});
递归锁
__block pthread_mutex_t recursiveMutex;
pthread_mutexattr_t recursiveMutexattr;
pthread_mutexattr_init(&recursiveMutexattr);
pthread_mutexattr_settype(&recursiveMutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&recursiveMutex, &recursiveMutexattr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
NSLog(@"线程加锁");
pthread_mutex_lock(&recursiveMutex);
if (value > 0) {
NSLog(@"value: %d",value);
RecursiveBlock(--value);
}
NSLog(@"线程解锁");
pthread_mutex_unlock(&recursiveMutex);
};
RecursiveBlock(3);
});
读写锁
typedef void(^ReadWriteBlock)(NSString *str);
typedef void(^VoidBlock)(void);
__block pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
__block NSMutableArray *arrayM = [NSMutableArray array];
ReadWriteBlock writeBlock = ^ (NSString *str) {
NSLog(@"开启写操作");
pthread_rwlock_wrlock(&rwlock);
[arrayM addObject:str];
sleep(2);
pthread_rwlock_unlock(&rwlock);
};
VoidBlock readBlock = ^ {
NSLog(@"开启读操作");
pthread_rwlock_rdlock(&rwlock);
sleep(1);
NSLog(@"读取数据:%@",arrayM);
pthread_rwlock_unlock(&rwlock);
};
for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
writeBlock([NSString stringWithFormat:@"%d",I]);
});
}
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
readBlock();
});
}
dispatch_barrier_async / dispatch_barrier_sync
现在有一个需求:任务1,2,3 均执行完毕执行任务 0,然后执行任务4,5,6,我们通过 GCD
的 barrier 方法来实现,如下:
- (void)dispatch_barrierTest {
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lc.brrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"任务1 -- %@", [NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"任务2 -- %@", [NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"任务3 -- %@", [NSThread currentThread]);
});
dispatch_barrier_sync(concurrentQueue, ^{
NSLog(@"任务0 -- %@", [NSThread currentThread]);
sleep(5); //默认耗时
});
NSLog(@"dispatch_barrier 测试");
dispatch_async(concurrentQueue, ^{
NSLog(@"任务4 -- %@", [NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"任务5 -- %@", [NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"任务6 -- %@", [NSThread currentThread]);
});
}
运行,输出结果如下:
可以看到,任务0执行是在任务1,2,3 都执行完之后才会执行,而任务4,5,6是在任务0执行后才会执行(其中1,2,3 是不分先后顺序,同样的4,5,6也不分先后顺序)
dispatch_barrier_async 和 dispatch_barrier_sync 的区别
相同点
-
等待前面的任务都执行完毕才会执行当前的任务
-
当前任务执行完毕才会执行后面的任务
不同点
-
dispatch_barrier_async 将当前任务添加到队列之后,会将后续的任务也添加到队列中,但是后面的任务只能等待当前任务执行完毕,才会执行后面的任务
-
dispatch_barrier_sync 将当前任务添加到队列之后,等待当前任务执行完毕,才会将后续的任务添加到队列,然后执行任务
将上述代码中的 dispatch_barrier_sync
换成 dispatch_barrier_async
后,输出结果为:
拓展
property - atomic & nonatomic
atomic 修饰的对象,系统会保证在其自动生成的 getter/setter 方法中的操作是完整的,不受其他线程的影响。
atomic
- 默认修饰符
- 会保证CPU能在别的线程访问这个属性之前先执行完当前操作
- 读写速度慢
- 线程不安全 - 如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
nonatomic
- 手动写
- 速度更快
- 线程不安全
- 如果两个线程同时访问会出现不可预料的结果
单例实现
单例:该类在程序运行期间有且仅有一个实例
使用 GCD 来实现
- (id)shareInstance {
static id shareInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!shareInstance) {
shareInstance = [[NSObject alloc] init];
}
});
return shareInstance;
}
通过 pthread 来实现
- (void)lock {
pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_once(&once, onceFunc);
}
void onceFunc() {
static id shareInstance;
shareInstance = [[NSObject alloc] init];
}