文章目录
锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用
注:不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了
iOS中锁的基本种类只有两种:互斥锁、自旋锁,其他的可能比如:条件锁、递归锁、信号量都是上层的封装和实现
自旋锁:
我们在weak的实现原理中有学习过自旋锁,对于每个SideTable中间都有自旋锁,同时也使用了分离锁给单个SideTable上锁。
自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直到显式释放自旋锁。
自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
自旋锁的种类
1. OSSpinLock
自从OSSpinLock
出现了安全问题之后就废弃了。自旋锁之所以不安全,是因为自旋锁由于获取锁时,线程会一直处于忙则等待的死锁状态,造成了任务优先级的反转
OSSpinLock
忙等的机制就可能造成高优先级一直running等待
,占用CPU时间片,而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
2. os_unfair_lock
weak实现部分的自旋锁就使用的是这个,自旋锁已经不安全了,苹果推出了os_unfair_lock
,这个锁解决了优先级反转的问题
//创建一个锁
os_unfair_lock_t unfairLock;
//初始化
unfairLock = &(OS_UNFAIR_LOCK_INIT);
//加锁
os_unfair_lock_lock(unfairLock);
//解锁
os_unfair_lock_unlock(unfairLock);
互斥锁
进行互斥操作的锁,防止两条线程同时对同一公共资源(比如全局变量)进行读写操作。
互斥锁又分为:
- 递归锁:可重入锁,同一个线程在锁匙放钱可再次获取锁,即可以递归调用
- 非递归锁:不可重入,必须等锁释放后才能再次获取锁
对于递归锁我们要注意使用时死锁问题,前后代码相互等待就会死锁
对于非递归锁,我们强行使用递归就会造成堵塞而非死锁。
1. pthread_mutex
pthread_mutex就是互斥锁本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠
// 导入头文件
#import <pthread.h>
// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);
2. @synchronized
@synchronized通过调用C++的recursive_mutex_lock进行上锁和解锁的步骤。
@synchronized需要一个参数,这个参数相当于信号量
//用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {
@synchronized (self) {
_testInt = testInt;
}
}
@synchronized的底层实现
- 从其注释中
recursive mutex
可以得出synchronized
是递归锁 - 如果锁的对象
obj
不存在时分别会走objc_sync_nil()
和不做任何操作
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
这也就是@synchronized作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁
3. 正常情况下(obj存在)会通过id2data
方法生成一个SyncData
对象
- nextData指的是链表中下一个SyncData
- object指的是当前加锁的对象
- threadCount表示使用该对象进行加锁的线程数
- mutex即对象所关联的锁
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
- 既然@synchronized能在任意地方(VC、View、Model等)使用,那么底层必然维护着一张全局的表(类似于weak表)。而从SyncList和SyncData的结构可以证实系统确实在底层维护着一张哈希表,里面存储着SyncList结构的数据。SyncList和SyncData的关系如下图所示:
- 使用快速缓存。这里有个重要的知识点——
TLS
:TLS
全称为Thread Local Storage
,在iOS中每个线程都拥有自己的TLS,负责保存本线程的一些变量, 且TLS无需锁保护。
快速缓存的含义:定义两个变量SYNC_DATA_DIRECT_KEY/SYNC_COUNT_DIRECT_KEY
,与tsl_get_direct/tls_set_direct
配合可以从线程局部缓存中快速取得SyncCacheItem.data
和SyncCacheItem.lockCount
如果在缓存中找到当前对象,就拿出当前被锁的次数lockCount,再根据传入参数类型(获取、释放、查看)对lockCount分别进行操作
- 获取资源ACQUIRE:lockCount++并根据key值存入被锁次数
- 释放资源RELEASE:lockCount–并根据key值减少被锁次数。如果次数变为0,此时锁也不复存在,需要从快速缓存移除并清空线程数threadCount
- 查看资源check:不操作
lockCount表示被锁的次数,意味着能多次进入,从侧面表现出了递归性
- 慢速缓存查找。获取该线程下的SyncCache。这个逻辑分支是找不到确切的线程标记只能进行所有的缓存遍历
typedef struct {
SyncData *data; //该缓存条目对应的SyncData
unsigned int lockCount; //该对象在该线程中被加锁的次数
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated; //该缓存此时对应的缓存大小
unsigned int used; //该缓存此时对应的已使用缓存大小
SyncCacheItem list[0]; //SyncCacheItem数组
} SyncCache;
- SyncCacheItem用来记录某个SyncData在某个线程中被加锁的记录,一个SyncData可以被多个SyncCacheItem持有
- SyncCache用来记录某个线程中所有SyncCacheItem,并且记录了缓存大小以及已使用缓存大小
- 全局哈希表查找。快速、慢速流程都没找到缓存就会来到这步——在系统保存的哈希表进行链式查找。
- lockp->lock()并不是在底层对锁进行了封装,而是在查找过程前后进行了加锁操作
- for循环遍历链表,如果有符合的就goto done。寻找链表中未使用的SyncData并作标记
- 如果是RELEASE或CHECK直接goto done
- 如果第二步中有发现第一次使用的的对象就将threadCount标记为1且goto done
- 生成新数据并写入缓存
第三步情况均不满足(即链表不存在——对象对于全部线程来说是第一次加锁)就会创建SyncData并存在result里,方便下次进行存储
done分析:
- 先将前面的lock锁解开
- 如果是RELEASE类型直接返回nil
- 对ACQUIRE类型和对象的断言判断
- !fastCacheOccupied分支表示支持快速缓存且快速缓存被占用了,将该SyncCacheItem数据写入快速缓存中
- 否则将该SyncCacheItem存入该线程对应的SyncCache中
其他问题:
- 不能使用非OC对象作为加锁条件——id2data中接收参数为id类型
- 多次锁同一个对象会有什么后果吗——会从高速缓存中拿到data,所以只会锁一次对象
- 都说@synchronized性能低——是因为在底层增删改查消耗了大量性能
- 加锁对象不能为nil,否则加锁无效,不能保证线程安全
- (void)test {
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
}
上面代码一运行就会崩溃,原因是因为在某一瞬间testArray释放了为nil,但哈希表中存的对象也变成了nil,导致synchronized无效化
解决方案:1. self同步锁 2. NSLock
3. NSLock
NSLock是非递归锁
NSLock是对互斥锁的简单封装
- (void)test {
self.testArray = [NSMutableArray array];
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
self.testArray = [NSMutableArray array];
[lock unlock];
});
}
}
NSLock在AFNetworking的AFURLSessionManager中有使用到
其就是对互斥锁的简单封装
如果对非递归锁强行使用递归调用,就会在调用时发生堵塞,并非死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。(因为不会查询缓存)
从官方文档的解释里看的更清楚,在同一线程上调用NSLock的两次lock方法将永久锁定线程。同时官方文档重点提醒向NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。
4. NSRecursiveLock
NSRecursiveLock是递归锁
- (void)test {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
}
如果我们在外层添加for循环
- (void)test {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
}
}
此时程序会crush
因为for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁,也就是找不到解锁的出口。
即线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁
此时我们可以通过@synchronized对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性。
同一线程可以多次获取而不会导致死锁的锁。
注意是同一线程
5. GCD信号量
6. NSCondition
- NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lock和unlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样
- 同时,NSCondition提供更高级的用法。wait和signal,和条件信号量类似。比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。
- NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。
但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.
7. NSConditionLock
- lock不分条件,如果锁没被申请,直接执行代码,unlock不会清空条件,之后满足条件的锁还会执行
- unlockWithCondition:我的理解就是设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)
- lockWhenCondition:满足特定条件,执行相应代码
- NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。
- NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。
- (void)executeNSConditionLock {
NSConditionLock* lock = [[NSConditionLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger i=0; i<3; i++) {
sleep(2);
if (i == 2) {
[lock lock];
[lock unlockWithCondition:i];
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
[lock lockWhenCondition:2];
[lock unlock];
}
总结
可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息称,苹果在新的系统中已经优化了 pthread_mutex 的性能,所有它看上去和 dispatch_semaphore 差距并没有那么大了。
OSSpinLock
不再安全,底层用os_unfair_lock
替代atomic
只能保证setter、getter时线程安全,所以更多的使用nonatomic
来修饰读写锁
更多使用栅栏函数来实现@synchronized
在底层维护了一个哈希链表进行data
的存储,使用recursive_mutex_t
进行加锁NSLock
、NSRecursiveLock
、NSCondition
和NSConditionLock
底层都是对pthread_mutex
的封装- NSCondition和NSConditionLock是条件锁,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似