回顾之前
前文讲到多线程原理,线程安全、线程阻塞、线程使用等;这节我们来分析一下有关线程安全的一部分:锁,线程锁。
锁初识
我们所用到的锁,是为了解决线程安全问题;一段代码段在同一个时间只能允许被有限个线程访问,解决资源竞争导致的数据紊乱;八大线程锁即:1.NSClock,2.NSConditionLock,3.NSCondition,4.NSRecursiveLock,5.@synchronized,6.dispatch_semaphore,7.OSSpinLock,8.pthread_mutex
线程资源竞争问题举例:
@property (nonatomic, assign) NSInteger totalNum;
-(void)viewDidLoad {
_totalNum = 10;
[self threadSecuretTest];
}
- (void)runMoreTickets{
if (_totalNum == 0) {
return;
}
sleep(0.2);
NSLog(@"%ld (currentThreadName:%@, currentThreadCount:%ld)",_totalNum --, [NSThread currentThread].name,[NSThread currentThread].stackSize);
}
- (void)threadSecuretTest {
dispatch_queue_t queuet = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queuet, ^{
for (int i = 0; i < 5; i++) {
[self runMoreTickets];
}
});
dispatch_async(queuet, ^{
for (int i = 0; i < 5; i++) {
[self runMoreTickets];
}
});
dispatch_async(queuet, ^{
for (int i = 0; i < 5; i++) {
[self runMoreTickets];
}
});
}
结果:** 2020-07-20 09:47:15.702720+0800 Test-OC[86022:6943874] 8 (currentThreadName:, currentThreadCount:524288)**
2020-07-20 09:47:15.702720+0800 Test-OC[86022:6943879] 10 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.702720+0800 Test-OC[86022:6943873] 9 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.702897+0800 Test-OC[86022:6943874] 6 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.702898+0800 Test-OC[86022:6943879] 7 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.702897+0800 Test-OC[86022:6943873] 6 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.703034+0800 Test-OC[86022:6943879] 4 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.703028+0800 Test-OC[86022:6943874] 5 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.703057+0800 Test-OC[86022:6943873] 3 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.703122+0800 Test-OC[86022:6943879] 2 (currentThreadName:, currentThreadCount:524288)
2020-07-20 09:47:15.703385+0800 Test-OC[86022:6943874] 1 (currentThreadName:, currentThreadCount:524288)
锁详解
1.NSClock
互斥锁,加锁过程是按照队列的形式(FIFO),先进先出的原则。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject {
@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
NSLock遵循NSLocking协议。lock是加锁,unLock是解锁,tryLock是尝试加锁,失败的话会返回NO;lockBeforeDate:是在指定时间前尝试加锁,要是在指定时间前加锁失败则返回NO。
看个例子
// 主线程
NSLock * lock = [[NSLock alloc] init];
dispatch_queue_t group = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 1
dispatch_async(group, ^{
[lock lock];
sleep(3);
NSLog(@"线程1");
[lock unlock];
NSLog(@"线程1解锁成功");
});
// 2
dispatch_async(group, ^{
sleep(2);
[lock lock];
NSLog(@"线程2");
[lock unlock];
});
2020-07-20 10:23:55.627833+0800 Test-OC[86238:6963970] 线程1
2020-07-20 10:23:55.628053+0800 Test-OC[86238:6963970] 线程1解锁成功
2020-07-20 10:23:55.628061+0800 Test-OC[86238:6963971] 线程2
由上面打印结果可以看出,线程1加锁时阻塞了线程2,线程二加锁失败。3s后线程1解锁了,线程二才加锁成功;
建议使用tryLock ,尝试加锁,成功返回YES,再使用解锁,这样不会阻塞线程。
如果是三个线程,那么一个线程在加锁的时候,其余请求锁的线程将形成一个等待队列,按先进先出原则,这个结果可以通过修改线程优先级进行测试得出。
2.NSConditionLock
和NSLock类似,都遵循NSLocking协议,不过多了condition属性。
@interface NSConditionLock : NSObject {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@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;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
举个栗子:
NSConditionLock * conditionLock = [[NSConditionLock alloc] initWithCondition:0];
dispatch_queue_t group = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 1
dispatch_async(group, ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[conditionLock unlock];
});
// 2
dispatch_async(group, ^{
sleep(1);
if ([conditionLock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[conditionLock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
}else{
NSLog(@"线程2尝试加锁失败");
}
});
// 3
dispatch_async(group, ^{
sleep(2);
if ([conditionLock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[conditionLock unlock];
NSLog(@"线程3解锁成功");
}else{
NSLog(@"线程3尝试加锁失败");
}
});
// 4
dispatch_async(group, ^{
sleep(3);
if ([conditionLock tryLockWhenCondition:2]) {
NSLog(@"线程4");
[conditionLock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
}else{
NSLog(@"线程4尝试加锁失败");
}
});
2020-07-20 13:53:10.877963+0800 Test-OC[88096:7132297] 线程2
2020-07-20 13:53:10.878170+0800 Test-OC[88096:7132297] 线程2解锁成功
2020-07-20 13:53:11.876597+0800 Test-OC[88096:7132296] 线程3
2020-07-20 13:53:11.876787+0800 Test-OC[88096:7132296] 线程3解锁成功
2020-07-20 13:53:12.878735+0800 Test-OC[88096:7132295] 线程4
2020-07-20 13:53:12.878914+0800 Test-OC[88096:7132295] 线程4解锁成功
2020-07-20 13:53:12.878923+0800 Test-OC[88096:7132298] 线程1
上述代码先输出线程2而没先输出线程1,由于condition1开始处于未满足处于解锁状态,会使线程1处于waiting状态,到线程4解锁出1时才满足条件;tryLockWhenCondition 即使未满足条件也不会返回NO,不会阻塞当前线程。
可以看出,NSConditionLock还可实现任务依赖关系
3.NSCondition
NScondition的实例化对象作为锁和锁的检查器使用;不像其他锁一样,先轮询,而是直接进入waiting状态当有其他 线程执行signal或者broadcast方法时,线程被唤醒,继续之后的方法;锁上之后其他线程仍然可以上锁,之后可以根据条件判断是否继续运行线程,即线程是否进入waiting状态。
@interface NSCondition : NSObject {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
举例用法:
NSCondition * condition = [[NSCondition alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *testArr = [NSMutableArray array];
dispatch_async(queue, ^{
[condition lock];
while (!testArr.count) {
[condition wait];
}
NSLog(@"线程1");
[testArr removeAllObjects];
NSLog(@"testArr removeAllObject");
[condition unlock];
});
dispatch_async(queue, ^{
[condition lock];
[testArr addObject:@1];
NSLog(@"线程2, textArrAddObject");
[condition signal];
[condition unlock];
});
2020-07-20 14:39:39.001432+0800 Test-OC[88547:7161017] 线程2, textArrAddObject
2020-07-20 14:39:39.001750+0800 Test-OC[88547:7161016] 线程1
2020-07-20 14:39:39.001881+0800 Test-OC[88547:7161016] testArr removeAllObject
上面结果可以看出,condition上锁后还是可以继续上锁,并不会阻塞线程;使用场景更多在锁定条件对象,测试是否安全的执行以下任务。
其中signal 和broadcast 方法区别在于,signal是信号量控制,调用一次只能唤起一次线程等待;想要唤醒多次需要多次调用;broadcast则是可以唤醒所有线程等待。若无线程等待,调用两方法都无作用。
4.NSRecursiveLock
递归锁,他与NSLock区别在于,NSRecursiveLock可以在一个线程里重复加锁(由于单线程是按顺序执行,不会出现资源竞争的情况);NSRecursiveLock会记住上锁和解锁的次数,只有平衡了才会释放锁。其他线程才能上锁成功
**@interface** NSRecursiveLock : NSObject {
**@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**
使用举例:
NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
**static** **void** (^RecursiveLockBlock)(**int**);
RecursiveLockBlock = ^(**int** value){
[lock lock];
**if** (value > 0) {
NSLog(@"Value: %d",value);
RecursiveLockBlock(value - 1);
}
};
RecursiveLockBlock(3);
});
**2020-07-20 15:58:38.965564+0800 Test-OC[89057:7199031] Value: 3**
**2020-07-20 15:58:38.965825+0800 Test-OC[89057:7199031] Value: 2**
**2020-07-20 15:58:38.965924+0800 Test-OC[89057:7199031] Value: 1**
从上可知,使用NSRecursiveLock 在加锁未解锁情况下重复加锁而不会阻塞线程,要是替换成NSLock加锁未解锁继续加锁就会阻塞线程,下面代码就不会执行了;递归锁就是为了解决这种问题。
5.@sychronized
对象级别锁,互斥锁。@sychronized(object) ,只有object相同情况下才满足互斥条件。
举个
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
**@synchronized** (**self**) {
NSLog(@"线程1");
}
});
dispatch_async(queue, ^{
sleep(2);
**@synchronized** (**self**) {
NSLog(@"线程2");
}
});
@synchronized (object
) 用法比较简单,在方法内已经处理好加解锁;不过性能相对是较差的那种;
6.dispatch_semaphore
信号量,控制线程最大并发数,也是锁的一种使用
dispatch_semaphore_t
// 传入值必须
>=0
, 若传入为0
则阻塞线程并等待timeout,时间到后会执行其后的语句dispatch_semaphore_create(long value);
// 可以理解为
lock
,会使得signal
值-1
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
可以理解为
unlock
,会使得signal
值+1
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
使用举例:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 2);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"线程1 线程等待");
dispatch_semaphore_wait(semaphore, overTime);
NSLog(@"线程1");
dispatch_semaphore_signal(semaphore);
NSLog(@"线程1 signal发信号");
});
dispatch_async(queue, ^{
NSLog(@"线程2 线程等待");
dispatch_semaphore_wait(semaphore, overTime);
NSLog(@"线程2");
dispatch_semaphore_signal(semaphore);
NSLog(@"线程2 signal发信号");
});
**2020-07-23 16:39:02.668485+0800 Test-OC[2783:214582] 线程2 线程等待**
**2020-07-23 16:39:02.668485+0800 Test-OC[2783:214584] 线程1 线程等待**
**2020-07-23 16:39:02.668794+0800 Test-OC[2783:214584] 线程1**
**2020-07-23 16:39:02.668812+0800 Test-OC[2783:214582] 线程2**
**2020-07-23 16:39:02.668920+0800 Test-OC[2783:214584] 线程1 signal发信号**
**2020-07-23 16:39:02.668911+0800 Test-OC[2783:214582] 线程2 signal发信号**
从上述结果可以看出,在线程等待后才开始后续的方法执行;类似于教室座位计算,人满了外面会处于等待状态,有座位才安排;设置dispatch_semaphore_create(0) 为0 时overTime生效,等待满足实际才开始执行下面的任务;
7.OSSPinkLock
自旋锁,效率为最高(iOS10之后被官方认定为不安全的锁,不建议使用)自旋锁不会让等待的进入睡眠状态
需导入头文件
// #import
使用举例:
**__block** OSSpinLock osLock = OS_SPINLOCK_INIT;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"线程1 准备上锁");
OSSpinLockLock(&osLock);
NSLog(@"线程1");
OSSpinLockUnlock(&osLock);
NSLog(@"线程1 解锁");
});
dispatch_async(queue, ^{
NSLog(@"线程2 准备上锁");
OSSpinLockLock(&osLock);
NSLog(@"线程2");
OSSpinLockUnlock(&osLock);
NSLog(@"线程2 解锁");
});
**2020-07-26 23:48:49.448361+0800 Test-OC[7829:456571] 线程1 准备上锁**
**2020-07-26 23:48:49.448361+0800 Test-OC[7829:456570] 线程2 准备上锁**
**2020-07-26 23:48:49.448656+0800 Test-OC[7829:456570] 线程2**
**2020-07-26 23:48:49.448770+0800 Test-OC[7829:456570] 线程2 解锁**
**2020-07-26 23:48:49.448951+0800 Test-OC[7829:456571] 线程1**
**2020-07-26 23:48:49.449080+0800 Test-OC[7829:456571] 线程1 解锁**
从运行结果可以看出我们锁住线程1和线程二时,线程二会一直处于线程等待状态,直到线程1解锁完成,线程二会立即执行;
如果我们改变一些解锁状态看一下运行是否受影响
/// 将线程一解锁关闭
dispatch_async(queue, ^{
NSLog(@"线程1 准备上锁");
OSSpinLockLock(&osLock);
NSLog(@"线程1");
// OSSpinLockUnlock(&osLock);
NSLog(@"线程1 解锁");
}); ....
**2020-07-27 00:03:05.802912+0800 Test-OC[8003:465228] 线程1 准备上锁**
**2020-07-27 00:03:05.802912+0800 Test-OC[8003:465230] 线程2 准备上锁**
**2020-07-27 00:03:05.803140+0800 Test-OC[8003:465228] 线程1**
**2020-07-27 00:03:05.803245+0800 Test-OC[8003:465228] 线程1 解锁**
可以看出线程1未解锁线程2也不会执行,所以oslock
里 lock
和 unlock
需要成对存在
OS_SPINLOCK_INIT: 默认值为
0
,在locked
状态时就会大于0
,unlocked
状态下为0
OSSpinLockLock(&oslock):上锁,参数为
OSSpinLock
地址OSSpinLockUnlock(&oslock):解锁,参数为
OSSpinLock
地址OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回
YES
,反之返回NO
8.pthread_mutex
递归锁, ibireme在《不再安全的 OSSpinLock》文章中提到,性能最好的OSSPinkLock不再安全已将osspinklock
替换为pthread_mutex
需提前导入头文件:
// #import "pthread.h"
使用举例:
static pthread_mutex_t p_lock;
- (void)pthread_mutex_t_Text{
pthread_mutex_init(&p_lock, NULL);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"线程1 准备加锁");
pthread_mutex_lock(&p_lock);
sleep(2);
NSLog(@"线程1");
pthread_mutex_unlock(&p_lock);
NSLog(@"线程1 解锁成功");
});
dispatch_async(queue, ^{
NSLog(@"线程2 准备加锁");
pthread_mutex_lock(&p_lock);
sleep(2);
NSLog(@"线程2");
pthread_mutex_unlock(&p_lock);
NSLog(@"线程2 解锁成功");
});
}
2020-07-27 10:08:05.713354+0800 Test-OC[8616:492609] 线程2 准备加锁
2020-07-27 10:08:05.713354+0800 Test-OC[8616:492610] 线程1 准备加锁
2020-07-27 10:08:07.717438+0800 Test-OC[8616:492610] 线程1
2020-07-27 10:08:07.717653+0800 Test-OC[8616:492610] 线程1 解锁成功
2020-07-27 10:08:09.720069+0800 Test-OC[8616:492609] 线程2
2020-07-27 10:08:09.720386+0800 Test-OC[8616:492609] 线程2 解锁成功
从运行结果可以看出,原理和osspinkLock类似,不过ossPinkLocktry(&lock)
和pthread_mutex_trylock
区别在于后者返回的是0
,否则就是一个错误提示码,前者则返回YES
/NO
YYCache中pthread_mutex_t用法:
pthread_mutex_t(recursive)
pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&pLock, &attr);
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用
pthread_mutex还有一种递归锁的用法,从上面用法可知,一般需要加锁之后只能一个线程对象访问,不解锁下个线程是不可访问的。递归锁,可以在一个线程内重复加锁而不释放
使用举例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
static void (^recursiveBlock)(int);
recursiveBlock = ^(int value){
pthread_mutex_lock(&p_lock);
if (value > 0) {
NSLog(@"value: %d",value);
recursiveBlock(value-1);
}
pthread_mutex_unlock(&p_lock);
};
recursiveBlock(7);
});
2020-07-27 10:58:07.534516+0800 Test-OC[9103:517900] value: 7
2020-07-27 10:58:07.534677+0800 Test-OC[9103:517900] value: 6
2020-07-27 10:58:07.534803+0800 Test-OC[9103:517900] value: 5
2020-07-27 10:58:07.534899+0800 Test-OC[9103:517900] value: 4
2020-07-27 10:58:07.534992+0800 Test-OC[9103:517900] value: 3
2020-07-27 10:58:07.535128+0800 Test-OC[9103:517900] value: 2
2020-07-27 10:58:07.535833+0800 Test-OC[9103:517900] value: 1
总结
参考:https://www.jianshu.com/p/b3ab3d390903