上篇文章只讲了OSSpinLock,本文继续讲iOS中其他加锁方案。
二. os_unfair_lock
- os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持,从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁
- 需要导入头文件#import <os/lock.h>
#import "OSUnfairLockDemo.h"
#import <os/lock.h>
@interface OSUnfairLockDemo()
@property (assign, nonatomic) os_unfair_lock moneyLock;
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end
@implementation OSUnfairLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.moneyLock = OS_UNFAIR_LOCK_INIT;
self.ticketLock = OS_UNFAIR_LOCK_INIT;
}
return self;
}
// 死锁:永远拿不到锁
- (void)__saleTicket
{
os_unfair_lock_lock(&_ticketLock);
[super __saleTicket];
os_unfair_lock_unlock(&_ticketLock);
}
- (void)__saveMoney
{
os_unfair_lock_lock(&_moneyLock);
[super __saveMoney];
os_unfair_lock_unlock(&_moneyLock);
}
- (void)__drawMoney
{
os_unfair_lock_lock(&_moneyLock);
[super __drawMoney];
os_unfair_lock_unlock(&_moneyLock);
}
@end
os_unfair_lock可以进行尝试加锁:os_unfair_lock_trylock(os_unfair_lock_t lock);
等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,所以如果忘记解锁了,那么当下一个线程进来的时候发现没有解锁,就会休眠,当所有的线程都进来 ,就会导致所有的线程都会休眠,这种情况称为死锁,就是永远拿不到锁。
三. pthread_mutex
- mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
- pthread_mutex是跨平台的,一般pthread开头的都是跨平台的,iOS、Linux、Windows等都能使用
- 需要导入头文件#import <pthread.h>
#import "MutexDemo.h"
#import <pthread.h>
@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t ticketMutex;
@property (assign, nonatomic) pthread_mutex_t moneyMutex;
@end
@implementation MutexDemo
- (void)__initMutex:(pthread_mutex_t *)mutex
{
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 初始化锁,传空就相当于PTHREAD_MUTEX_DEFAULT
//这一行代码就相当于上面的代码
//pthread_mutex_init(mutex, NULL);
}
- (instancetype)init
{
if (self = [super init]) {
[self __initMutex:&_ticketMutex];
[self __initMutex:&_moneyMutex];
}
return self;
}
- (void)__saleTicket
{
//尝试加锁
//int pthread_mutex_trylock(pthread_mutex_t *);
pthread_mutex_lock(&_ticketMutex);
[super __saleTicket];
pthread_mutex_unlock(&_ticketMutex);
}
- (void)__saveMoney
{
pthread_mutex_lock(&_moneyMutex);
[super __saveMoney];
pthread_mutex_unlock(&_moneyMutex);
}
- (void)__drawMoney
{
pthread_mutex_lock(&_moneyMutex);
[super __drawMoney];
pthread_mutex_unlock(&_moneyMutex);
}
- (void)dealloc
{
//销毁锁
pthread_mutex_destroy(&_moneyMutex);
pthread_mutex_destroy(&_ticketMutex);
}
@end
pthread_mutexattr_settype有三种类型,一般我们只用默认的锁、递归锁,如下:
#define PTHREAD_MUTEX_NORMAL 0 默认的锁
#define PTHREAD_MUTEX_ERRORCHECK 1 检查错误的锁
#define PTHREAD_MUTEX_RECURSIVE 2 递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL 默认的锁
- 代码中,如果传NULL,就代表是默认锁。
- 也可以进行尝试加锁:pthread_mutex_trylock(pthread_mutex_t *);
- 在dealloc中销毁锁。
下面介绍递归锁的作用:
如下代码:
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
[self otherTest2];
pthread_mutex_unlock(&_mutex);
}
- (void)otherTest2
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
pthread_mutex_unlock(&_mutex);
}
上面代码otherTest和otherTest2中使用了同一把锁。当一个线程进入otherTest,发现没加锁,然后它加上了一把锁。当再进入otherTest2的时候,发现这把锁锁上了,就休眠等待解锁,但是想要解锁就要otherTest执行完,想要执行完otherTest就要执行otherTest2,这样就循环等待造成了死锁。
由于上面是两个不同的方法,解决办法也很简单,每个方法各自使用一把锁就可以解决死锁。
但是如果是如下代码呢?
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
//递归调用10次
static int count = 0;
if (count < 10) {
count++;
[self otherTest];
}
pthread_mutex_unlock(&_mutex);
}
otherTest方法递归调用10次,这里不能使用两把锁了,这里只能使用递归锁,解决死锁。
递归锁:允许同一个线程对一把锁进行重复加锁(解锁)
代码如下:
#import "MutexDemo2.h"
#import <pthread.h>
@interface MutexDemo2()
@property (assign, nonatomic) pthread_mutex_t mutex;
@end
@implementation MutexDemo2
- (void)__initMutex:(pthread_mutex_t *)mutex
{
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// 传入PTHREAD_MUTEX_RECURSIVE,就是递归锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
}
- (instancetype)init
{
if (self = [super init]) {
[self __initMutex:&_mutex];
}
return self;
}
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
//递归调用10次
static int count = 0;
if (count < 10) {
count++;
[self otherTest];
}
pthread_mutex_unlock(&_mutex);
}
- (void)dealloc
{
pthread_mutex_destroy(&_mutex);
}
@end
总结:
- 递归调用产生死锁,用递归锁解决
- 递归锁:允许同一个线程对一把锁进行重复加锁(解锁)
可能你会想,允许对一把锁进行重复加锁不就不安全了吗?其实因为是同一个线程访问,其他线程在等待,所以是安全的。
比如:线程1递归调用otherTest三次。调用第一次,加锁1,调用第二次加锁2,调用第三次加锁3。然后解锁第一次,解锁第二次,解锁第三次。只有当otherTest全部解锁之后线程2才可以访问,如果没有全部解锁那么线程2会一直在休眠等待。
线程1:
otherTest(+-)
otherTest(+-)
otherTest(+-)
线程2:
otherTest(等待)
四. 自旋锁、互斥锁汇编代码分析
1. 自旋锁汇编代码分析
修改代码:
先创建10条线程来卖票
/**
卖票演示
*/
- (void)ticketTest
{
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
}
.......
}
将卖票等待的时间变600s:
/**
卖1张票
*/
- (void)__saleTicket
{
int oldTicketsCount = self.ticketsCount;
sleep(.600);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}
打断点:
- (void)__saleTicket
{
OSSpinLockLock(&_ticketLock); //在这里打断点
[super __saleTicket];
OSSpinLockUnlock(&_ticketLock);
}
运行:
- (void)viewDidLoad {
[super viewDidLoad];
MJBaseDemo *demo = [[OSSpinLockDemo alloc] init];
[demo ticketTest];
// [demo moneyTest];
// [demo otherTest];
}
如下图:
可以发现,首先是线程7进入__saleTicket,这时候没有加锁,线程7加锁之后,线程10再进入__saleTicket,这时候已经加锁,线程10处于忙等状态,我们就在这里停下,通过汇编观察忙等状态都发生了什么。
选择Always show disassembly,永远显示汇编。
指令:
step instruction 一行一行汇编代码执行,如果有函数,会进入函数一行一行执行 (简称si)
step 一行一行OC代码执行(会一次过好多汇编代码)
next instruction 一行一行汇编代码执行,如果有函数,会一次性调用完函数,然后往下走。
输入指令si,一直敲回车。
(lldb) si
(lldb) si
......
(lldb) si
(lldb)
来到如下代码:
0x10d9208d1 <+12>: cmpl $-0x1, %eax
0x10d9208d4 <+15>: jne 0x10d9208f1 ; <+44>
0x10d9208d6 <+17>: testl %ecx, %ecx
0x10d9208d8 <+19>: je 0x10d9220f9 ; _OSSpinLockLockYield
0x10d9208de <+25>: pause
0x10d9208e0 <+27>: incl %ecx
0x10d9208e2 <+29>: movl (%rdi), %eax
0x10d9208e4 <+31>: testl %eax, %eax
-> 0x10d9208e6 <+33>: jne 0x10d9208d1 ; <+12>
第一行的地址是0x10d9208d1,最后一行也会跳到0x10d9208d1,可以看出这是个while循环。当判断到锁一直在加锁状态,就会走这一段while循环,线程处于忙等状态,直至解锁。
总结:
验证了OSSpinLock是自旋锁,这也验证了自旋锁的本质是while循环。
2. 互斥锁汇编代码分析
打断点:
- (void)__saleTicket
{
os_unfair_lock_lock(&_ticketLock); //这里打断点
[super __saleTicket];
os_unfair_lock_unlock(&_ticketLock);
}
运行:
- (void)viewDidLoad {
[super viewDidLoad];
MJBaseDemo *demo = [[OSUnfairLockDemoalloc] init];
[demo ticketTest];
// [demo moneyTest];
// [demo otherTest];
}
同理,线程8会先进入__saleTicket,这时候没有加锁,线程8加锁之后,线程9再进入__saleTicket,这时候已经加锁,线程9处于休眠状态,我们就在这里停下,通过汇编观察休眠状态都发生了什么。
同样,敲si指令:
在如下函数打断点,c 继续来到下面函数:(c是continue的简称)
0x101a8feb1 <+33>: callq 0x101a921ee ; symbol stub for: os_unfair_lock_lock
si进去:
-> 0x101a921ee <+0>: jmpq *0x1ebc(%rip) ; (void *)0x0000000104ae146e: os_unfair_lock_lock
发现真调用了这个函数。
在如下函数打断点,c 继续来到下面函数:
0x104ae1481 <+19>: jmp 0x104ae1bcb ; _os_unfair_lock_lock_slow
si进去,找到如下函数,打断点:
0x104ae1c4f <+132>: callq 0x104ae65a6 ; symbol stub for: __ulock_wait
c继续来到这个函数,si进去,再si,最后会来到:
0x104a9f9dc <+8>: syscall
可以发现,最后来到了syscall系统调用,syscall调用之后模拟器就自动弹出了,说明线程不做事情了,线程休眠去了。
总结:
验证了os_unfair_lock是互斥锁,互斥锁是通过线程休眠实现的。同理也可以验证pthread_mutex是互斥锁,验证方式和上面类似,这里就省略了。
补充:
High-Level Lock:高级锁
特点:会一直等待,不会休眠。
比如:OSSpinLock
Low-Level Lock:低级锁
特点:等不到锁就休眠。
比如:os_unfair_lock、pthread_mutex
五. pthread_mutex – 条件锁的用法
上面说了,如果pthread_mutexattr_settype类型传PTHREAD_MUTEX_DEFAULT或者NULL就是默认锁,如果传PTHREAD_MUTEX_RECURSIVE就是递归锁。
下面讲一下pthread_mutex – 条件锁
上面我们讲的解锁都是通过unlock解锁,如果使用了条件就不一定通过unlock也能解锁。
假设需求是,我们有两个线程,一条线程是删除东西,一条线程是添加东西,删东西和添加东西都要加锁,删除东西时候有个条件,就是必须要有东西才进行删除,那么如何实现?
#import "MutexDemo3.h"
#import <pthread.h>
@interface MutexDemo3()
@property (assign, nonatomic) pthread_mutex_t mutex; //锁
@property (assign, nonatomic) pthread_cond_t cond; //条件
@property (strong, nonatomic) NSMutableArray *data; //数组
@end
@implementation MutexDemo3
- (instancetype)init
{
if (self = [super init]) {
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
//GUNstep源码里面就是传入PTHREAD_MUTEX_ERRORCHECK,我也模仿着做了
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_ERRORCHECK);
// 初始化锁
pthread_mutex_init(&_mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 初始化条件
pthread_cond_init(&_cond, NULL);
self.data = [NSMutableArray array];
}
return self;
}
- (void)otherTest
{
//一个线程删除东西
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
//一个线程添加东西
[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}
// 线程1
// 删除数组中的元素
- (void)__remove
{
pthread_mutex_lock(&_mutex);
NSLog(@"__remove - begin");
//如果数组为0,就先等等,等有东西了再删除
if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}
[self.data removeLastObject];
NSLog(@"删除了元素");
pthread_mutex_unlock(&_mutex);
}
// 线程2
// 往数组中添加元素
- (void)__add
{
pthread_mutex_lock(&_mutex);
sleep(1);
[self.data addObject:@"Test"];
NSLog(@"添加了元素");
// 信号 唤醒上面那个线程
pthread_cond_signal(&_cond);
// 广播 唤醒所有等待这个条件的线程
// pthread_cond_broadcast(&_cond);
pthread_mutex_unlock(&_mutex);
}
- (void)dealloc
{
//销毁锁,销毁条件
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
@end
注意:等待和唤醒必须是同一个条件
上面的代码:
- 如果线程1先进来__remove删除方法,会先加一把锁,然后发现没有东西可删,就让线程睡觉,这时候这把锁就会暂时解开。
- 然后线程2进来__add添加方法,由于上面暂时解锁了,线程2发现没锁,就加锁,然后添加东西,添加完东西通过signal唤醒线程1,线程1被唤醒之后又会拿到这把锁加锁,然后线程1继续执行代码,执行完代码线程1解锁,线程2执行完自己的东西也会解锁。最后的结果是线程1加锁解锁两次,线程2加锁解锁一次。
打印:
__remove - begin
添加了元素
删除了元素
可以发现,就算先进来的是remove方法,也会先添加元素再删除元素。
应用场景:
线程1依赖线程2,需要线程2做完某件事再回到线程1继续做事情。比如厂家和消费者的关系,消费者要想买东西,必须要厂家先生产东西。
六. NSLock、NSRecursiveLock、NSCondition
NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装
1. NSLock
NSLock相关API:
@protocol NSLocking
- (void)lock; //加锁
- (void)unlock; //解锁
@end
@interface NSLock : NSObject <NSLocking> {
//尝试加锁
- (BOOL)tryLock;
//传入一个时间,在这个时间之前等待这把锁放开(阻塞),如果等到这把锁放开了,就加锁,加锁成功返回YES。如果在规定时间内别人还是没把这把锁放开,就加锁失败,返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
//锁的名字
@property (nullable, copy) NSString *name;
@end
可以看出NSLock继承于NSObject,遵守了NSLocking协议,NSLocking协议有两个方法:加锁、解锁。
使用方法很简单,如下:
#import "NSLockDemo.h"
@interface NSLockDemo()
@property (strong, nonatomic) NSLock *ticketLock;
@property (strong, nonatomic) NSLock *moneyLock;
@end
@implementation NSLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.ticketLock = [[NSLock alloc] init];
self.moneyLock = [[NSLock alloc] init];
}
return self;
}
- (void)__saleTicket
{
[self.ticketLock lock];
[super __saleTicket];
[self.ticketLock unlock];
}
- (void)__saveMoney
{
[self.moneyLock lock];
[super __saveMoney];
[self.moneyLock unlock];
}
- (void)__drawMoney
{
[self.moneyLock lock];
[super __drawMoney];
[self.moneyLock unlock];
}
@end
如何验证NSLock是对mutex普通锁的封装?
如果使用打断点,一句一句查看汇编,会先进入objc_msgSend的找lock方法的过程,比较麻烦,我们直接看GNUstep的实现:
在NSLock.m文件找到如下代码:
+ (void) initialize
{
......
pthread_mutex_init(&deadlock, &attr_normal); //传入的是normal,普通锁
pthread_mutex_lock(&deadlock);
......
}
上面代码验证了NSLock是对mutex普通锁的封装。
2. NSRecursiveLock
NSRecursiveLock是对mutex递归锁的封装,它的API和NSLock一样。
NSRecursiveLock相关API:
@interface NSRecursiveLock : NSObject <NSLocking> {
//尝试加锁
- (BOOL)tryLock;
//传入一个时间,在这个时间之前等待这把锁放开(阻塞),如果等到这把锁放开了,就加锁,加锁成功返回YES。如果在规定时间内别人还是没把这把锁放开,就加锁失败,返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
//锁的名字
@property (nullable, copy) NSString *name;
@end
使用方法就省略了,下面验证NSRecursiveLock是对mutex递归锁的封装。
也是在GNUstep的NSLock.m文件里面,找到NSRecursiveLock的init方法实现:
- (id) init
{
if (nil != (self = [super init]))
{
if (0 != pthread_mutex_init(&_mutex, &attr_recursive)) //递归锁
{
DESTROY(self);
}
}
return self;
}
可以发现,NSRecursiveLock的确是对mutex递归锁的封装。
3. NSCondition
NSCondition是对mutex和cond的封装。
NSCondition相关API:
@interface NSCondition : NSObject <NSLocking> {
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name;
@end
我们可以在GNUstep的NSLock.m文件里面找到源码实现:
- (void) signal
{
pthread_cond_signal(&_condition);
}
- (void) wait
{
pthread_cond_wait(&_condition, &_mutex);
}
- (BOOL) waitUntilDate: (NSDate*)limit
{
NSTimeInterval ti = [limit timeIntervalSince1970];
double secs, subsecs;
struct timespec timeout;
int retVal = 0;
// Split the float into seconds and fractions of a second
subsecs = modf(ti, &secs);
timeout.tv_sec = secs;
// Convert fractions of a second to nanoseconds
timeout.tv_nsec = subsecs * 1e9;
/* NB. On timeout the lock is still held even through condition is not met
*/
retVal = pthread_cond_timedwait(&_condition, &_mutex, &timeout);
if (retVal == 0)
{
return YES;
}
if (retVal == ETIMEDOUT)
{
return NO;
}
if (retVal == EINVAL)
{
NSLog(@"Invalid arguments to pthread_cond_timedwait");
}
return NO;
}
- (void) broadcast
{
pthread_cond_broadcast(&_condition);
}
可以发现,上面封装的API的确是上面我们讲过的条件锁调用的API,验证了:NSCondition是对mutex和cond的封装。
NSCondition使用起来和mutex的条件锁是一样的,不解释了,如下:
#import "NSConditionDemo.h"
@interface NSConditionDemo()
@property (strong, nonatomic) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;
@end
@implementation NSConditionDemo
- (instancetype)init
{
if (self = [super init]) {
self.condition = [[NSCondition alloc] init];
self.data = [NSMutableArray array];
}
return self;
}
- (void)otherTest
{
//一个线程删除
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
//一个线程添加
[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}
// 线程1
// 删除数组中的元素
- (void)__remove
{
[self.condition lock];
NSLog(@"__remove - begin");
if (self.data.count == 0) {
// 等待
[self.condition wait];
}
[self.data removeLastObject];
NSLog(@"删除了元素");
[self.condition unlock];
}
// 线程2
// 往数组中添加元素
- (void)__add
{
[self.condition lock];
sleep(1);
[self.data addObject:@"Test"];
NSLog(@"添加了元素");
// 信号
[self.condition signal];
// 广播
//[self.condition broadcast];
[self.condition unlock];
}
打印也是:
__remove - begin
添加了元素
删除了元素
补充:
上面代码是先发送signal,再unlock的,其实先unlock再发送signal也可以。
- 先发signal,再unlock:线程2发送信号,线程1收到信号的时候,想要加锁,但是发现还没解锁,线程1继续等待,直到线程2解锁。然后线程1才开始执行接收到信号后要做的事,加锁、删除、解锁。
- 先发unlock,再signal:线程2先解锁,再发送信号,线程1收到信号的时候已经解锁了,然后线程1开始执行接收到信号后要做的事,加锁、删除、解锁。
这两行代码如果很近,就没啥区别,如果中间有很多其他代码,那就看你自己想达成什么效果了。
Demo地址:加锁方案-1