锁在我们开发中用的相对比较少,但是作为一个开发者,还是需要了解锁的原理;
下图是锁的性能数据图:
锁的归类
- 自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释 放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。
- 互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比 如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区 而达成
- 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行
- 递归锁:就是同一个线程可以加锁N次而不会引发死锁
- 信号量:是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥
- 读写锁:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作,这种锁相对于自旋锁而言,能提高并发性,它允许同时有多个读者来访问共享资源。
其实基本的锁就包括了三类 自旋锁 互斥锁 读写锁,
其他的比如条件锁,递归锁,信号量都是上层的封装和实现!
- 互斥锁在上图包括:NSLock,pthread_mutex,@synchronized
- 条件锁有:NSCondition,NSConditionLock
- 递归锁:NSRecursiveLock,pthread_mutex(recursive)
- 信号量:dispatch_semaphore
互斥锁与递归锁
在开发中,我们常用的大概是@synchronized
,就从这个开始讲解;
下面使用@synchronized
来举一个例子:
锁的应用是为了线程的安全执行,例如购票,不同线程购票不加锁的话,会出现同一张票被卖多次。
下面看一段代码:
- (void)saleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount)
}else{
NSLog(@"当前车票已售罄");
}
}
}
通过@synchronized
对票数的减少进行加锁之后,我们执行程序后,就不会出现问题。
下图是执行后的打印结果:
那既然@synchronized
能够保证线程的安全,那就先去看一下它的底层原理;
首先创建一个iOS工程,在main.m
函数中加上@synchronized (appDelegateClassName) { }
这句代码,对main函数进行xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
转换成cpp
文件。
接下来就看一下main函数中的@synchronized
转化之后变成的代码块:
{
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);}
}
可以看到它有一个objc_sync_enter
和_sync_exit
;
通过对objc_sync_enter
的断点查看,得知@synchronized
的底层在libobjc.A.dylib
中。
下图是objc_sync_enter
的源码实现,可以看到它传入了obj
之后,就会进行处理,而没有传入时,就会调用objc_sync_nil()
函数,这个函数没有任何实现。
也就是说,当@synchronized(nil)
括号中传入的是nil,它什么都不做,无法起作用。
在有值的情况下,会对mutex
进行lock()
加锁函数的调用,那么看一下objc_sync_exit
的函数实现:
可以看到在objc_sync_exit
中会对mutex
调用tryUnlock()
解锁函数。
那么重点就是SyncData
了,它是一个结构体:
而结构体中最重要的就是recursive_mutex_t
:
它是一个recursive_mutex_tt
类,在这个类中有lock
和unlock
两个方法,是一个递归锁。
那么回到objc_sync_enter
,锁的创建是由id2data
创建的,这个函数代码很多,下图是一部分代码,主要功能是通过kvc的方式拿到data值;
在if(data)
判断中,有对ACQUIRE
和RELEASE
的方法实现:
其中lockCount
表示被锁了多少次,可重入,比递归锁功能更强大,因为在查看SyncData
时,它是一个链表结构。
上面的代码是在SUPPORT_DIRECT_THREAD_KEYS
的情况下的,而不在这个情况下,也跟上面的代码差不多;
SUPPORT_DIRECT_THREAD_KEYS
是从线程栈缓存的形式,而endif
是从cache
的形式获取去缓存。
而如果第一次加载时,就会从下面的代码执行:
也就是说,第一次进来,会通过kvc对tls进行设值和标记,设置threadCount = 1
,lockCount = 1
,在线程栈存空间和缓存空间中都会进行处理。
那么一个完整的流程也就很清晰了。
总结:@synchronized整个流程其实就是一张哈希表,因为底层封装的是recursive_mutex_t
,所以是一把递归锁,扩展了递归锁里面增加了lockCount
,防止多线程的重入,增加了threadCount
进行处理。
@synchronized
这把锁的性能是比较低的,因为里面有很多链表的查询,缓存,下层代码的查找,导致了性能是比较差;但是为什么用的多呢,因为方便简单,好用。
下面来看一段代码:
for (int i = 0; i < 200; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
这一段代码是使用异步函数创建数组对象,这一段代码是有问题的,因为不断的初始化导致了问题,没销毁就创建,多个线程创建同一个对象,释放一次还好,释放多次,僵尸对象,就造成野指针。
那么防止问题的产生可以进行加锁;例如使用@synchronized()
,那么括号内的参数可以填什么呢?
如果是填_testArray
,那么还是会存在问题,因为存在某一个临界点,_testArray
会变成nil,那么锁nil就会出问题;可以进行锁self,因为self
是持有者,它是有一个生命周期的。
那么除了用@synchronized
,在性能和对objc的生命周期不明确时,还可以使用NSLock
;
在创建数组前执行[lock lock]
,在创建之后执行[lock unlock]
;
下面来研究一下NSRecursiveLock
和NSLock
这两把锁的使用,看下面一段代码:
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; 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);
});
}
这段代码进行了函数的嵌套调用,会产生递归,那么如何加锁使其不产生递归呢?
通常一般情况下,我们会在方法执行前加锁和方法执行结束进行解锁,也就是说在testMethod
函数前调用[lock lock]
在testMethod(10)
之后执行[lock unlock]
;那这样做产生的后果就是它会循环10次从10到1的结果,这是一种解决方法;
那如果在testMethod
方法里的if
判断外加锁在if
判断外解锁,这样会产生循环递归问题,因为Lock
是一把简单的互斥锁,当执行testMethod
进行加锁,之后又调用testMethod
,又一次加锁,也就是不同的线程进行加锁,当想要解锁时,因为其他线程已经加锁了,需要等待其他线程进行解锁。
那么NSLock
是无法解决这种递归的特性,使用@synchronized
也是可以的,但是效果也是执行10次从10到1。
那么使用NSRecursiveLock
这把递归锁,是可以很好的解决这个问题的。
在testMethod
方法前调用[recursiveLock lock];
在if
判断结束后调用[recursiveLock unlock];
是可以解决问题的。
NSRecursiveLock
和Lock
的底层都是在pthread
的基础上进行封装的;而他们的代码实现大部分是一样的,但是NSRecursiveLock
的初始化的地方与NSLock
是有区别的,看下面两张图片:
底层源码是来自swift的Foundation框架。
那么关于NSLock
和NSRecursiveLock
的使用就介绍完了,总的来说,使用是比较麻烦的,在使用便捷方面,@synchronized
更简单、实用,而这两把锁就比较复杂,但是性能会比较高一些。
条件锁
下面来研究一下NSCondition
这把锁;
1:[condition lock];//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
2:[condition unlock];//与lock 同时使用
3:[condition wait];//让当前线程处于等待状态
4:[condition signal];//CPU发信号告诉线程不用在等待,可以继续执行
NSCondition的对象实际上作为一个锁和一个线程检查器:锁主要 为了当检测条件时保护数据源,执行条件引发的任务;线程检查器 主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
它适用于一个生产消费者模型;
看代码:
- (void)testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
}
}
- (void)producer{
[_testCondition lock]; // 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; // 信号
[_testCondition unlock];
}
- (void)consumer{
[_testCondition lock]; // 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
上面的代码执行流程需要先生产出来,才能消费,当消费多了,还没生产完,就需要等待。
那么为了避免多线程的影响,需要对生产的时候进行加锁,防止它还为生产完,就进行消费了,当生产完了,发送一个信号,再进行解锁。
那么新增需求,当我们需要对事务进行顺序处理时,如何使用锁来处理;那就要介绍一下NSConditionLock
这把条件锁了,
看代码:
// 信号量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
// -[NSConditionLock lockWhenCondition: beforeDate:]
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@"线程 2");
// self.myLock.value = 1;
[conditionLock unlockWithCondition:1]; // _value = 2 -> 1
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
这把锁的特殊性在于这把锁是有一个条件,通过设置信号量lockWhenCondition
,在执行时会对信号量进行匹配,首先执行线程3,因为它没有设置信号量,然后信号量进行匹配,由于初始化时设置了信号量为2,因此先执行线程2,在里面又设置了解锁信号量为1,就匹配线程1,如果解锁信号量与线程1的信号量不匹配,那么线程1不会执行。
注意,线程3与线程2的顺序有可能是不确定的,如果线程3比较耗时,那么有可能会先执行线程2,再执行线程1,最后执行线程3.
那么关于其他的一些锁这边就不再介绍了。