手动目录
- 什么是锁
- 锁的工作机制
- 锁的分类
- 设计到锁的其他概念
- 常用锁的用法
@synchronized()
NSLock
信号量 dispatch_semaphore_t
atomic
条件锁(NSConditionLock 、NSCondition)
读写锁 pthread_rwlock- 对比理解递归/非递归
- 更多内容参考文章
在不再安全的 OSSpinLock一文中 提到以下问题
1、列举出9种不同的锁(不完全)
2、分析不同锁的执行效率- 测试代码
3、提出OSSpinLock不一定安全:优先级反转------高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock
什么是锁
锁是一种同步机制,用于多线程环境中对资源访问的限制,保护数据安全。
锁只是用来保证数据的安全,不保证执行顺序
锁的工作机制
每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待或循环访问,直到锁重新可用。
锁的分类
- 自旋锁:
如果共享数据已经有其他线程加锁了,线程会以死循环的方式一直尝试去访问,一旦被访问的资源被解锁(忙等),则等待资源的线程会立即执行。 - 互斥锁
分为递归锁/非递归锁
如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
自旋锁的效率高于互斥锁。但是比互斥锁耗性能
自旋锁:atomic、OSSpinLock、dispatch_semaphore_t 、pthread_rwlock(读写锁)
互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock,os_unfair_lock
os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。
注意 @synthesize 和 @ synchronized 的写法
设计到锁的其他概念
临界区
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。读写锁:百度百科
一种特殊的自旋锁,
它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
能提高并发性,可以有多个线程来同时读,但是只允许一个线程来写。条件锁
互斥锁的一种。
有一个条件,当进程的某些资源要求不满足这个条件时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。递归锁
互斥锁的一种
同一个线程对同一把锁加锁多次,而不会引发阻塞。
同一个线程必须保证,加锁的次数和解锁的次数相同,其他线程才能够顺利解锁非递归锁
互斥锁的一种
在不解锁的情况下,当同一个线程多次加锁时,会产生阻塞。
常用锁的用法
@synchronized()
递归锁
- 一般用法
- (void)task5 { dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT); for (NSInteger i = 0 ; i < 1000; i ++) { dispatch_async(t, ^{ @synchronized (self) { NSLog(@"----%@",@(i)); // 打印顺序 不固定 [self.array addObject:@(1)]; } }); } NSLog(@"3"); // 打印顺序穿插在 中间 }
在 关于 @synchronized,这儿比你想知道的还要多这篇文章中讲了具体源码的实现。
同时,你也可以自己通过源码去分析。,通过clang命令将 @synchronized (self) {} 编译成底层源码。可以找到入口函数
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj); // 开始锁
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {
objc_sync_exit(sync_exit);
}
id sync_exit;
}
_sync_exit(_sync_obj); // 释放锁
} catch (id e) {_rethrow = e;}
{
struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
}_fin_force_rethow(_rethrow);
}
}
在源码阅读中,有一些点需要注意:
1、 @synchronized (obj) 中的obj 作为锁的标识符,只有标识符相同,才能满足互斥条件
。
2、如果obj传入的是空(null),那么内部不做任务处理,也就是没有进行加锁解锁操作。
因为是递归锁,我们可以写类似这样的代码:
- (void)testLock{
if(_count>0){
@synchronized (obj) {
_count = _count - 1;
[self testLock];
}
}
}
而如果换成 NSLock ,它就会因为递归发生死锁了。
NSLock
非递归锁
NSLock 属于 pthread_mutex 的一层封装, 设置了属性为nil
pthread_mutex_init(mutex,nil) --- 在swift-corefoundation 开源代码里能看到。
也有资料说nil 实际上是PTHREAD_MUTEX_ERRORCHECK
它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。
NSLock的API非常简单
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
lock:加锁
unlock:解锁
trylock:能加锁返回 YES 并执行加锁操作,相当于 lock,反之返回 NO,相当于判断当前锁的状态
lockBeforeDate:这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁操作并返回 YES,反之返回 NO
name: 设置锁的名称
NSLock的用法
- (void)task5 {
dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
NSLock *lock = [NSLock new];
for (NSInteger i = 0 ; i < 1000; i ++) {
dispatch_async(t, ^{
[lock lock];
NSLog(@"----%@",@(i));
[self.array addObject:@"1"];
[lock unlock];
});
}
NSLog(@"------------3");
}
信号量 dispatch_semaphore_t
其用法在 上一篇文章 iOS底层--GCD应用 的信号量有讲解,不再赘述。
atomic
atomic 用于声明属性的时候的修饰符。与之对应的是 nonatomic。
atomic相对于 nonatomic 来说,能够保证数据安全,但是执行效率低(因为内部要加锁,大概慢20倍)。一般不实用这个来修饰属性。
关于 atomic 的知识点,可能涉及到2个面试题:
1、atomic的原理
2、atomic一定是安全的吗?
- atomic的原理
在 objc 源码中 通过 objc_setProperty_atomic
找到 真正的实现源码 reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil]; // copy 修饰 进行copy操作
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil]; // mutableCopy 修饰 进行mutableCopy操作
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue); // 其他 修饰 进行retain操作
}
if (!atomic) { // 非原子修饰 直接新值替换掉旧值
oldValue = *slot;
*slot = newValue;
} else { // ⚠️ 原子修饰
spinlock_t& slotlock = PropertyLocks[slot]; // mutex_tt --- os_unfair_lock 锁
slotlock.lock(); // 加锁
oldValue = *slot;
*slot = newValue;
slotlock.unlock(); // 解锁
}
objc_release(oldValue);
}
通过源码一目了然,内部通过 mutex_tt 加锁 在临界区内进行旧置换新值的操作。
get方法同样可以找到
- atomic 是否安全?
我们常说的 atomic 能保证数据安全,是给予对数据的读写(set/get)来说的,对于数据的修改,其并不能保证数据安全
。
比如说NSString *,我们可以在多线程中进行读写(set/get),是安全的。
比如 NSMutableArray、NSMutableDictionary,进行读写(set/get)是安全的,但是如果对数据进行修改,就不再是安全的(增、删、改)。 - 安全的写 --- 没有问题
@property (atomic, strong) NSMutableArray *array;
- (void)task5 {
dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0 ; i < 1000; i ++) {
dispatch_async(t, ^{
_array = [NSMutableArray new];
});
}
}
// 这种写法 如果 @property (nonatomic, strong) NSMutableArray *array; 就是不安全的
- 不安全的改 -- 数据不安全
@property (atomic, strong) NSMutableArray *array;
_array = [NSMutableArray new];
- (void)task5 {
dispatch_queue_t t = dispatch_queue_create("je", DISPATCH_QUEUE_CONCURRENT);
// NSLock *lock = [NSLock new];
for (NSInteger i = 0 ; i < 1000; i ++) {
dispatch_async(t, ^{
// [lock lock];
[self.array addObject:@"1"];
// [lock unlock];
});
}
}
NSRecursiveLock - 递归锁
NSRecursiveLock 也是对 pthread_mutex 的一层封装, 设置了属性为PTHREAD_MUTEX_RECURSIVE
、
--- 在swift-corefoundation 开源代码里能看到。
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
条件锁(NSConditionLock 、NSCondition)
每次提到条件锁,我们一般用生产者和消费者老举例:
在一个面包店里:面包师生产面包,将面包放入货架,消费者从货架上拿走面包,前提是这个货架上有面包才能拿走。在谈 iOS的锁一文中的条件变量中 也进行了类似的解释。
在这篇文章中特别提到:(用循环测试 也会出现这个问题)
实际操作 NSCondition 做 wait 操作时,如果用 if 判断,是不能保证消费者是线程安全的。
--- (文做中搜索 <生产者-消费者问题> 有具体解释)
if(count==0){
[condition wait];
}
所以为了保证消费者操作的正确,使用 while 循环中的判断
,进行二次确认:
while (count == 0) {
[condition wait];
}
--- NSCondition
非递归锁
NSCondition 属于 pthread_mutex 的一层封装, 设置了属性为nil
--- 在swift-corefoundation 开源代码里能看到。
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
看到这里是不是觉得很熟悉? 和 NSLock
一样。不过是在此基础上 加上了一个条件标示。
NSCondition 遵循NSLocking协议,具体API
@interface NSCondition : NSObject <NSLocking> {
- (void)wait; //进程进入等待状态
- (BOOL)waitUntilDate:(NSDate *)limit; //线程等待一定的时间
- (void)signal; //唤醒一个等待的线程
- (void)broadcast; //唤醒所有等待的线程
@property (nullable, copy) NSString *name;
先用代码建立生产/消费 面包的场景
@property (nonatomic, assign) NSInteger number; // 面包数量
self.number = 0; // 初始化面包数量为0
// 开始上班工作
- (void)task6 {
for (NSInteger i = 0; i < 1000; i ++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self takeAwayOne]; // 消费者来买面包
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self productOne]; // 生产一个面包
});
}
}
- (void)takeAwayOne { // 拿走一个面包
self.number --;
NSLog(@"拿走一个,剩余%ld",self.number);
}
- (void)productOne { // 生产一个面包
self.number ++ ;
NSLog(@"------生产一个,剩余%@",@(self.number));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self task6];
}
多执行几次touchues ,发现有时候是先拿走,再生产,这样的结果显然不是我们理想的结果。
为了解决这个问题,我们需要用锁来处理。
生产/消费 都加锁,在因为消费的前提是有生产,所以在消费里面 wait、在生产里面发信号 signal。
@property (nonatomic, strong) NSCondition *condition; //条件锁
_condition = [[NSCondition alloc]init]; // 初始化锁
- (void)takeAwayOne {
[_condition lock];
while (self.number == 0) {
[_condition wait];
}
self.number --;
NSLog(@"拿走一个,剩余%ld",self.number);
[_condition unlock];
}
- (void)productOne {
[_condition lock];
self.number ++ ;
NSLog(@"------生产一个,剩余%@",@(self.number));
[_condition unlock];
[_condition signal];
}
这样就能满足我们实际的场景。
在生产者中, 是先 unlock 还是先signal? 我认为是都可以的。
至于 broadcast 在什么情况下用? 从上面的例子看,如果生产者一次生产2个面包,但是消费者只买一个,那么我如果用signal ,就是生产2个,只让一个消费者来买走一个,剩下的一个 放在那里,其他想买的人都以为没有面包了,就一直等,是影响实际生产的。
--- NSConditionLock
NSConditionLock 其实是对NSCondition的封装 在此基础上添加一个 value(NSInteger)的一个条件
-- 在swift-corefoundation 开源中能找到这样一段:
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition() // 内用用的就是 NSCondition
internal var _value: Int // 在NSCondition的基础上 加了 value的条件
....
_cond.broadcast() // 内部的发送信号 用的是广播,而不是 signal 。 一般来说 broadcast 比signal的效率低。
_cond.unlock()
.....
}
看其API
@protocol NSLocking
- (void)lock; // 不管什么条件 直接锁
- (void)unlock; // 不管什么条件 直接解锁
@end
- (instancetype)initWithCondition:(NSInteger)condition; // 初始化新分配的NSConditionLock对象并设置其条件
@property (readonly) NSInteger condition; // 条件 (只读)
- (void)lockWhenCondition:(NSInteger)condition; // 根据条件加锁
- (BOOL)tryLock; // 能否加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition; // 如果接收器的条件等于指定的条件,则尝试获取锁。
- (void)unlockWithCondition:(NSInteger)condition; // 放弃锁定并设置接收器的条件
- (BOOL)lockBeforeDate:(NSDate *)limit; // 尝试在指定时间之前获取锁。
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; // 尝试在指定时间之前获取锁。(condition 是匹配条件)
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
特别注意的是:
unlockWithCondition:(NSInteger)condition 是放弃锁并重新设置条件。
用为代码来理解流程
[_conditionLock lockWhenCondition:0];
类似于:
if (value == 0) {
[ _conditionLock lock]; // 符合条件 就进行加锁
...... 执行操作
}
[_conditionLock unlockWithCondition:0];
类似于:
[ _conditionLock unlock]; // 解锁
value = 0; // 重设条件
[_conditionLock broadcast]; // 发送广播 (告诉其他的线程任务, 服务value = 0 的可以执行了)
}
类似于银行取号办业务: 有20个人,每个人的号分别是 1---20;
_conditionLock = [[NSConditionLock alloc]initWithCondition:1]; // 开始办业务了, 当前叫的号是1号[_conditionLock lockWhenCondition:5]; // 我持有5号,当前不是我的号 我要等着(直到有人通知 5号可以办业务 我才可以办)
.......... 5号要办的业务
[_conditionLock unlockWithCondition:7]; // 5号业务办完了, 我要通知7号办业务[_conditionLock lockWhenCondition:1]; // 我持有1号
.......... 1号要办的业务
[_conditionLock unlockWithCondition:2]; // 我的业务办完 了 通知2号来办业务。
读写锁 pthread_rwlock
读写锁的效果是:可以很对线程读,但是只能一个线程来写。
那么原理就应该是这样的:异步读数据,栅栏写数据
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT); // 自定义一个并发队列
- (id)readData:(id)key {
__block id obj;
dispatch_async(self.concurrent_queue, ^{
/// 读取数据
});
return obj;
}
- (void)setDataObject:(id)obj forKey:(NSString *)key{
dispatch_barrier_async(self.concurrent_queue, ^{
// 设置数据
});
}
对比理解递归/非递归
递归 ----- 同一个线程对同一把锁加锁多次,而不会引发阻塞。
非递归 -- 同一个线程多次加锁时,会产生阻塞。
NSLock -----非递归
NSRecursiveLock -----递归
@synchronized() -----递归
用这段代码来测试:
- (void)task5 {
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 定义一个block 任务
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10); // 在同一个线程进行多次加锁/解锁
});
}
// 打印结果
11:33:00.347074+0800 [7720:95610] current value = 10
只有这一条打印
NSLock 是非递归锁,在同一个线程下多次加锁解锁,就会出现阻塞现象
。
因为在 第一次执行testMethod(value - 1);
的时候 ,已经加了锁,再次执行这个代码的时候,还没有解锁,又进行了一次加锁,在没有解锁的情况下线程要进行休眠等待。 这样的话,这个线程就一直休眠,直到天荒地老。
为了解决这个问题,可以用递归锁。
方案一 :NSLock 换成 NSRecursiveLock
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
方案二: 用 @synchronized
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
}
};
testMethod(10); // 在同一个线程进行多次加锁/解锁
});
NSRecursiveLock 与 @synchronized 都是递归锁,所以在同一个线程、未解锁的情况下,可以多次加锁而不会产生阻塞,可以正常执行。
额外注意:
上面的测试代码中,是在同一线程下进行多次加锁。
如果在不同线程下,进行多次加锁会怎么样?
- (void)task5 {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (NSInteger i = 0; i < 2000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 定义一个block 任务
static void (^testMethod)(int);
testMethod = ^(int value){
if (value == 10) {
NSLog(@"%@",[NSThread currentThread]);
}
[lock lock];
if (value > 0) {
testMethod(value - 1);
}
[lock unlock];
};
// 调用任务
testMethod(10);
});
}
}
这样就会产生死锁
。
在不同的线程,同一个锁进行多次加锁,在某一个时刻,就可能产生 上一个锁还没解锁,下一个锁又来加锁。
线程1: lock 等 线程2 的unlock
线程2: lock 等 线程1的 unlock
形成了相互等待,产生死锁。
-
如何解决上面的问题?
为了解决循环调用递归锁产生死锁,可以使用@ synchronized 来解决。
(代码就不上了)
为什么同样是递归锁,在不同线程循环调用的时候 NSRecursiveLock 不行,而@synchronized 可以?
源码分析@synchronized 之后会发现 传入的参数(obj)的作用:
当第一次进行synch加锁的时候,将信息存入syncdata里面,放入syncList 表里
当第二次在进行加锁的时候,根据obj 来找,发现已经有锁了,就不进行lock 。直接用。
总结:
一般任务的锁:使用NSLock。
递归调用的锁:使用NSRecursiveLock
多线程 递归调用: 注意死锁的发生 。
更多内容参考文章:
谈 iOS 的锁🔥
深入理解 iOS 开发中的锁
iOS 开发中的八种锁(Lock)
iOS开发中的11种锁以及性能对比 🔥