高级IOS开发进阶 - 自旋锁、互斥锁以及读写锁(一)
一、锁的分类
在分析其它锁之前,需要先区分清楚锁的区别,基本的锁包括了二类:互斥锁 和 自旋锁。
1.1 自旋锁
自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种 忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
自旋锁 = 互斥锁 + 忙等。OSSpinLock
就是自旋锁。
1.2 互斥锁
互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
在Posix Thread
中定义有一套专⻔用于线程同步的mutex
函数,mutex
用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒(闲等)。
创建和销毁:
POSIX
定义了一个宏PTHREAD_MUTEX_INITIALIZER
来静态初始化互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
pthread_mutex_destroy ()
用于注销一个互斥锁。
锁操作相关API
:
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock()
语义与pthread_mutex_lock()
类似,不同的是在锁已经被占据时返回EBUSY
而不是挂起等待。
互斥锁 分为 递归锁 和 非递归锁
- 递归锁:
@synchronized
:多线程可递归。NSRecursiveLock
:不支持多线程可递归。pthread_mutex_t(recursive)
:多线程可递归。 - 非递归锁:
NSLock
、pthread_mutex
、dispatch_semaphore
、os_unfair_lock
。 - 条件锁:
NSCondition
、NSConditionLock
。 - 信号量(
semaphore
):是一种更高级的同步机制,互斥锁可以说是semaphore
在仅取值0/1
时的特例。信号量可以有更多的取值空间,用来实
现更加复杂的同步,而不单单是线程间互斥。dispatch_semaphore
1.2.1 读写锁
读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁 相对于自旋锁而言,能提高并发性。因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU
数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU
数相关),但不能同时既有读者又有写者,在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
一次只有一个线程可以占有写模式的读写锁,可以有多个线程同时占有读模式的读写锁。正是因为这个特性,当读写锁是写加锁状态时,在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。
通常当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁⻓期占用而导致等待的写模式锁请求⻓期阻塞。
读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为读模式锁定可以共享,写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。
创建和销毁API:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
成功则返回0
, 出错则返回错误编号。
同互斥锁一样, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy
对读写锁进行清理工作,释放由init
分配的资源。
锁操作相关API:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
0
, 出错则返回错误编号。这3
个函数分别实现获取读锁,获取写锁和释放锁的操作。获取锁的两个函数是阻塞操作,同样非阻塞的函数为:#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
非阻塞的获取锁操作,如果可以获取则返回0
, 否则返回错误的EBUSY
。
二、NSLock & NSRecursiveLock 的应用以及原理
2.1 案例一
__block NSMutableArray *array;
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
array = [NSMutableArray array];
});
}
对于上面的代码运行会发生崩溃,常规处理是对它加一个锁,如下:
__block NSMutableArray *array;
self.lock = [[NSLock alloc] init];
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.lock lock];
array = [NSMutableArray array];
[self.lock unlock];
});
}
这样就能解决array
的创建问题了。
2.2 案例二
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
上面的例子中最终输出会错乱:
可以在block
调用前后加解锁解决:
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
[self.lock lock];
testMethod(10);
[self.lock unlock];
});
}
但是在实际开发中锁往往是与业务代码绑定在一起的,如下:
这个时候block
在执行前会同一时间进入多次,相当于多次加锁了(递归),这样就产生了死锁。NSLog
只会执行一次。
将NSLock
改为NSRecursiveLock
可以解决NSLock
存在的死锁问题:
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[self.recursiveLock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[self.recursiveLock unlock];
};
testMethod(10);
});
}
但是在执行testMethod
一次(也有可能是多次)递归调用后没有继续输出:
由于NSRecursiveLock
不支持多线程可递归。所以改为@synchronized
:
for (int i = 0; i < 10; i++) {
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
解决了 NSLock
递归问题,@synchronized
解决了 NSRecursiveLock
多线程可递归问题问题。
2.3 原理分析
NSLock
与NSRecursiveLock
是定义在Foundation
框架中的,Foundation
框架并没有开源。有三种方式来探索:
- 分析
Foundation
动态库的汇编代码。 - 断点跟踪加锁解锁流程。
Swift Foundation
源码分析。虽然Foundation
框架本身没有,但是苹果开源了Swift Foundation
的代码。原理是想通的。swift-corelibs-foundation
当然有兴趣可以尝试编译可运行版本进行调试 swift-foundation 源码编译
在Foundation
中lock
与unlock
是NSLocking
协议提供的方法:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
在Swift Foundation
源码中同样有NSLocking
协议:
public protocol NSLocking {
func lock()
func unlock()
}
2.3.1 NSLock 源码分析
底层是对
pthread_mutex_init
的封装。lock
和unlock
同样是对pthread_mutex_lock
与pthread_mutex_unlock
的封装:Swift
的跨平台支持。2.3.2 NSRecursiveLock 源码分析
PTHREAD_MUTEX_RECURSIVE
的封装。lock
与unlock
同样是对pthread_mutex_lock
与pthread_mutex_unlock
的封装。三、NSCondition 原理
NSCondition
实际上作为一个 锁 和一个 线程检查器。锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
[condition lock]
:一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock
外等待,只到unlock
后才可访问。[condition unlock]
:与lock
同时使用。[condition wait]
:让当前线程处于等待状态。[condition signal]
:CPU
发信号告诉线程不用在等待,可以继续执行。
3.1 生产者-消费者 案例
- (void)testNSCondition {
//创建生产-消费者
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self test_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self test_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self test_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self test_producer];
});
}
}
- (void)test_producer{
[self.condition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产 + 1 剩余: %zd",self.ticketCount);
[self.condition signal]; // 信号
[self.condition unlock];
}
- (void)test_consumer{
[self.condition lock];
if (self.ticketCount == 0) {
NSLog(@"等待 剩余: %zd",self.ticketCount);
[self.condition wait];
}
//消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费 - 1 剩余: %zd ",self.ticketCount);
[self.condition unlock];
}
输出:
生产 + 1 剩余: 1
消费 - 1 剩余: 0
等待 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
等待 剩余: 0
等待 剩余: 0
等待 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
等待 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
等待 剩余: 0
等待 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
等待 剩余: 0
等待 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
等待 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
生产 + 1 剩余: 1
消费 - 1 剩余: 0
生产 + 1 剩余: 1
生产 + 1 剩余: 2
消费 - 1 剩余: 1
消费 - 1 剩余: 0
生产 + 1 剩余: 1
生产 + 1 剩余: 2
生产 + 1 剩余: 3
消费 - 1 剩余: 2
生产 + 1 剩余: 3
消费 - 1 剩余: 2
生产 + 1 剩余: 3
消费 - 1 剩余: 2
消费 - 1 剩余: 1
生产 + 1 剩余: 2
生产 + 1 剩余: 3
消费 - 1 剩余: 2
消费 - 1 剩余: 1
消费 - 1 剩余: 0
因为有condition
的存在保证了消费行为是在对应的生产行为之后。在这个过程中会有消费等待行为,signal
信号通知消费。
- 生产和消费的加锁保证了各个事务的额安全。
wait
和signal
保证了事务之间的安全。
3.2 源码分析
pthread_mutex_init
的包装,多了一个pthread_cond_init
。open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
open func wait() {
pthread_cond_wait(cond, mutex)
}
open func signal() {
pthread_cond_signal(cond)
}
open func broadcast() {
pthread_cond_broadcast(cond)
}
代码中去掉了windows
相关宏逻辑:
NSCondition
:锁(pthread_mutex_t
) + 线程检查器(pthread_cond_t
)- 锁(
pthread_mutex_t
):lock(pthread_mutex_lock)
+unlock(pthread_mutex_unlock)
- 线程检查器(
pthread_cond_t
):wait(pthread_cond_wait)
+signal(pthread_cond_signal)
四、NSConditionLock 使用和原理
NSConditionLock
也是锁,一旦一个线程获得锁,其他线程一定等待。它同样遵循NSLocking
协议,相关API
:
- (void)lockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
[conditionLock lock]
:表示conditionLock
期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition
) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁。[conditionLock lockWhenCondition:A条件]
:表示如果没有其他线程获得该锁,但是该锁内部的condition
不等于A
条件,它依然不能获得锁,仍然等待。如果内部的condition
等于A
条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。[conditionLock unlockWithCondition:A条件]
: 表示释放锁,同时把内部的condition
设置为A
条件。return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间]
: 表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程。注意:返回的值是NO
,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。- 所谓的
condition
就是整数,内部通过整数比较条件。
4.1案例
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(1);
NSLog(@"2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"3");
[conditionLock unlock];
});
上面的案例2
一定比1
先执行,2
与3
之间无序。
输出:3 2 1
,如果任务2
的优先级改为High
则输出顺序变为2 1 3
。
那么有以下疑问:
- 1.
NSConditionLock
与NSCondition
有关系么? - 2.
NSConditionLock
初始化的时候condition
是什么? - 3.
lockWhenCondition
是如何控制的? - 4.
unlockWithCondition
是如何控制的?
4.2 断点调试分析逻辑
在拿不到源码以及拿不到动态库的情况下,断点分析调用流程是一个比较好的方案。
分别在测试代码中打下以下断点:
运行工程到达断点后下符号断点-[NSConditionLock initWithCondition:]
过掉断点:
这个时候就进入了
initWithCondition
的汇编实现。在汇编中对所有的b
(跳转指令)下断点配合寄存器的值跟踪流程。-[NSConditionLock initWithCondition:]
:
- 可以通过
lldb
读取寄存器的值,也可以查看全部寄存器中对应的值。
过掉断点继续:
NSConditionLock
对象,它持有NSCondition
对象以及初始化传的condition
参数2
。-[NSConditionLock initWithCondition:]
流程:-[NSConditionLock initWithCondition:]
-[xxx init]
-[NSConditionLock init]
-[NSConditionLock zone]
+[NSCondition allocWithZone:]
-[NSCondition init]
-[NSConditionLock lockWhenCondition:]
:同样添加
-[NSConditionLock lockWhenCondition:]
符号断点:调用了
-[NSCondition unlock]
,这个时候继续过断点就又会回到线程4
,调用逻辑和线程3
相同。完整调用逻辑如下:
线程4
-[NSConditionLock lockWhenCondition:]
+[NSDate distantFuture]
-[NSConditionLock lockWhenCondition:beforeDate:]
-[NSCondition lock]
-[NSCondition waitUntilDate:]
线程3
-[NSConditionLock lockWhenCondition:]
+[NSDate distantFuture]
-[NSConditionLock lockWhenCondition:beforeDate:]
-[NSCondition lock]
-[NSCondition unlock]
返回1(true)
-[NSConditionLock unlockWithCondition:]
-[NSCondition lock]
-[NSCondition broadcast]
-[NSCondition unlock]
//回到线程4
-[NSCondition unlock]
返回1(true)
-[NSConditionLock unlockWithCondition:]
-[NSCondition lock]
-[NSCondition broadcast]
-[NSCondition unlock]
流程总结:
- 线程
4
调用[NSConditionLock lockWhenCondition:]
,此时因为不满足当前条件,所
以会进入waiting
状态,当前进入到waiting
时,会释放当前的互斥锁。 - 此时当前的线程
2
调用[NSConditionLock lock:]
,本质上是调用[NSConditionLock lockBeforeDate:]
这里不需要比对条件值,所以任务3
会执行。 - 接下来线程
3
执行[NSConditionLock lockWhenCondition:]
,因为满足条件值,所以线任务2
会执行,执行完成后会调用[NSConditionLock unlockWithCondition:]
,这个时候将condition
设置为1
,并发送boradcast
, 此时线程4
接收到当前的信号,唤醒执行并打印。 - 这个时候任务执行顺序为
任务3 -> 任务2 -> 任务1
。 [NSConditionLock lockWhenCondition:]
会根据传入的condition
进
行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行。[NSConditionLock unlockWithCondition:]
会先更改当前的condition
值,然后进行广播,唤醒当前的线程。
作者:HotPotCat
链接:https://www.jianshu.com/p/8f8e5f0d0b23