多线程的安全隐患
在使用多线程的过程中,一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,同一个变量,同一个对象,同一个文件。试想一下,三个线程同时向一个文件写东西,那势必会造成混乱。
下面以取钱存钱为例:
在这个例子中,起初余额中有1000,存钱的线程首先读出余额1000,紧接着取钱的线程又取出余额1000,然后存钱的线程又存入了1000,所以把余额修改为了2000,之后,取钱的线程取出了500,由于之前读出的余额是500,所以将余额修改为1000-500=500,这样最终的余额就变成了500。按照正常的情况,余额应该是1500,这样就出现了混乱。
起始票数是1000,第一个卖票的站点先读取的票的余额,过了一会第二个卖票的站点也读取了票的余额,然后第一个站点卖出了一张票,因此把票数余额修改为了999,过了一会第二个站点也卖了一张票,把票数余额修改为了999,这样一来,票就永远卖不完了。
我们用代码实现一下卖票的过程:
@property (nonatomic, assign)int ticketsCount;
- (void)saleTicket{
//这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
}
- (void)saleTickets{
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
打印结果:
2018-09-26 15:12:52.746209+0800 TEST[10226:312194] 最后还剩的票数13 线程{number = 5, name = (null)}
2018-09-26 15:12:52.746209+0800 TEST[10226:312193] 最后还剩的票数14 线程{number = 4, name = (null)}
2018-09-26 15:12:52.746245+0800 TEST[10226:312195] 最后还剩的票数14 线程{number = 3, name = (null)}
2018-09-26 15:12:52.746414+0800 TEST[10226:312194] 最后还剩的票数12 线程{number = 5, name = (null)}
2018-09-26 15:12:52.746552+0800 TEST[10226:312193] 最后还剩的票数11 线程{number = 4, name = (null)}
2018-09-26 15:12:52.746650+0800 TEST[10226:312195] 最后还剩的票数10 线程{number = 3, name = (null)}
2018-09-26 15:12:52.746707+0800 TEST[10226:312194] 最后还剩的票数9 线程{number = 5, name = (null)}
2018-09-26 15:12:52.746730+0800 TEST[10226:312193] 最后还剩的票数8 线程{number = 4, name = (null)}
2018-09-26 15:12:52.746913+0800 TEST[10226:312195] 最后还剩的票数7 线程{number = 3, name = (null)}
2018-09-26 15:12:52.747049+0800 TEST[10226:312194] 最后还剩的票数6 线程{number = 5, name = (null)}
2018-09-26 15:12:52.747301+0800 TEST[10226:312193] 最后还剩的票数5 线程{number = 4, name = (null)}
2018-09-26 15:12:52.747861+0800 TEST[10226:312194] 最后还剩的票数4 线程{number = 5, name = (null)}
2018-09-26 15:12:52.747861+0800 TEST[10226:312195] 最后还剩的票数4 线程{number = 3, name = (null)}
2018-09-26 15:12:52.748157+0800 TEST[10226:312193] 最后还剩的票数3 线程{number = 4, name = (null)}
2018-09-26 15:12:52.749157+0800 TEST[10226:312195] 最后还剩的票数2 线程{number = 3, name = (null)}
可以看到产生了混乱,最后剩余的票数并不为0。
然后继续用代码实现取钱存钱的过程
@property (nonatomic, assign)int money;
- (void)moneyTest{
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//存钱的线程
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self saveMoney];
}
});
//取钱的线程
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self drawmoney];
}
});
}
//存钱
- (void)saveMoney{
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
//取钱
- (void)drawmoney{
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
打印结果:
2018-09-26 15:27:13.265434+0800 TEST[10568:324343] 取20 还剩80元 - {number = 4, name = (null)}
2018-09-26 15:27:13.265459+0800 TEST[10568:324337] 存50 还剩150元 - {number = 3, name = (null)}
2018-09-26 15:27:13.265587+0800 TEST[10568:324337] 存50 还剩180元 - {number = 3, name = (null)}
2018-09-26 15:27:13.265589+0800 TEST[10568:324343] 取20 还剩130元 - {number = 4, name = (null)}
2018-09-26 15:27:13.265685+0800 TEST[10568:324337] 存50 还剩230元 - {number = 3, name = (null)}
2018-09-26 15:27:13.265693+0800 TEST[10568:324343] 取20 还剩210元 - {number = 4, name = (null)}
2018-09-26 15:27:13.265771+0800 TEST[10568:324337] 存50 还剩260元 - {number = 3, name = (null)}
2018-09-26 15:27:13.265853+0800 TEST[10568:324343] 取20 还剩240元 - {number = 4, name = (null)}
2018-09-26 15:27:13.266059+0800 TEST[10568:324337] 存50 还剩290元 - {number = 3, name = (null)}
2018-09-26 15:27:13.266210+0800 TEST[10568:324343] 取20 还剩270元 - {number = 4, name = (null)}
2018-09-26 15:27:13.266343+0800 TEST[10568:324337] 存50 还剩320元 - {number = 3, name = (null)}
2018-09-26 15:27:13.266485+0800 TEST[10568:324343] 取20 还剩300元 - {number = 4, name = (null)}
2018-09-26 15:27:13.266667+0800 TEST[10568:324337] 存50 还剩350元 - {number = 3, name = (null)}
2018-09-26 15:27:13.266844+0800 TEST[10568:324343] 取20 还剩330元 - {number = 4, name = (null)}
2018-09-26 15:27:13.267284+0800 TEST[10568:324337] 存50 还剩380元 - {number = 3, name = (null)}
2018-09-26 15:27:13.267373+0800 TEST[10568:324343] 取20 还剩360元 - {number = 4, name = (null)}
2018-09-26 15:27:13.267496+0800 TEST[10568:324337] 存50 还剩410元 - {number = 3, name = (null)}
2018-09-26 15:27:13.267866+0800 TEST[10568:324343] 取20 还剩390元 - {number = 4, name = (null)}
2018-09-26 15:27:13.268062+0800 TEST[10568:324337] 存50 还剩440元 - {number = 3, name = (null)}
2018-09-26 15:27:13.268578+0800 TEST[10568:324343] 取20 还剩420元 - {number = 4, name = (null)}
从最后剩余的钱数来看就完全不对,数据发生了明显的混乱。
那么多线程的安全隐患怎么解决呢?解决方案就是使用线程同步技术,常见的线程同步技术是加锁。
iOS中的线程同步方案有下面这些:
OSSpinLock
- OSSpinlock叫做"自旋锁",等待锁的线程会处于忙等状态,一直占用CPU资源
- 目前已经不再安全,可能会出现优先级反转的问题,即如果等待锁的线程优先级较高,它会一直占用着CPU的资源,优先级低的线程就无法释放锁。
关于OSSpinLock的API:
//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁看,如果需要等待就不加锁,直接返回false,如果不需要等待就加锁,返回true。
bool result = OSSpinLockTry(&lock);
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnlock(&lock);
下面我们使用OSSpinLock来解决卖票的资源争夺的问题:
- (void)saleTicket{
//加锁
OSSpinLockLock(&_lock);
//这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
//解锁
OSSpinLockUnlock(&_lock);
}
我们看一下打印结果:
2018-09-26 15:59:05.225340+0800 TEST[11218:345833] 最后还剩的票数14 线程{number = 3, name = (null)}
2018-09-26 15:59:05.225623+0800 TEST[11218:345833] 最后还剩的票数13 线程{number = 3, name = (null)}
2018-09-26 15:59:05.225799+0800 TEST[11218:345833] 最后还剩的票数12 线程{number = 3, name = (null)}
2018-09-26 15:59:05.225946+0800 TEST[11218:345833] 最后还剩的票数11 线程{number = 3, name = (null)}
2018-09-26 15:59:05.226248+0800 TEST[11218:345833] 最后还剩的票数10 线程{number = 3, name = (null)}
2018-09-26 15:59:05.227334+0800 TEST[11218:345826] 最后还剩的票数9 线程{number = 4, name = (null)}
2018-09-26 15:59:05.227480+0800 TEST[11218:345826] 最后还剩的票数8 线程{number = 4, name = (null)}
2018-09-26 15:59:05.227709+0800 TEST[11218:345826] 最后还剩的票数7 线程{number = 4, name = (null)}
2018-09-26 15:59:05.228151+0800 TEST[11218:345826] 最后还剩的票数6 线程{number = 4, name = (null)}
2018-09-26 15:59:05.233128+0800 TEST[11218:345826] 最后还剩的票数5 线程{number = 4, name = (null)}
2018-09-26 15:59:05.237517+0800 TEST[11218:345827] 最后还剩的票数4 线程{number = 5, name = (null)}
2018-09-26 15:59:05.238065+0800 TEST[11218:345827] 最后还剩的票数3 线程{number = 5, name = (null)}
2018-09-26 15:59:05.238499+0800 TEST[11218:345827] 最后还剩的票数2 线程{number = 5, name = (null)}
2018-09-26 15:59:05.239221+0800 TEST[11218:345827] 最后还剩的票数1 线程{number = 5, name = (null)}
2018-09-26 15:59:05.239897+0800 TEST[11218:345827] 最后还剩的票数0 线程{number = 5, name = (null)}
可以看到现在的输出没有任何问题了。
线程加锁的原理就是,当某一个线程首次访问资源时,对该资源加锁,当另外一个线程要访问该资源时首先判断锁有没有加上,没有的话就加锁然后访问资源,如果锁已经加上了,那么就会等待,等待锁打开。
下面再用OSSpinLock来完成存钱取钱的加锁:
//存钱
- (void)saveMoney{
OSSpinLockLock(&_lock);
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
OSSpinLockUnlock(&_lock);
}
//取钱
- (void)drawmoney{
OSSpinLockLock(&_lock);
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
OSSpinLockUnlock(&_lock);
}
看一下打印结果:
2018-09-26 16:45:14.317794+0800 TEST[12223:379269] 存50 还剩150元 - {number = 3, name = (null)}
2018-09-26 16:45:14.317953+0800 TEST[12223:379269] 存50 还剩200元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318071+0800 TEST[12223:379269] 存50 还剩250元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318182+0800 TEST[12223:379269] 存50 还剩300元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318374+0800 TEST[12223:379269] 存50 还剩350元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318500+0800 TEST[12223:379269] 存50 还剩400元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318587+0800 TEST[12223:379269] 存50 还剩450元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318689+0800 TEST[12223:379269] 存50 还剩500元 - {number = 3, name = (null)}
2018-09-26 16:45:14.318823+0800 TEST[12223:379269] 存50 还剩550元 - {number = 3, name = (null)}
2018-09-26 16:45:14.319047+0800 TEST[12223:379269] 存50 还剩600元 - {number = 3, name = (null)}
2018-09-26 16:45:14.320129+0800 TEST[12223:379270] 取20 还剩580元 - {number = 4, name = (null)}
2018-09-26 16:45:14.320242+0800 TEST[12223:379270] 取20 还剩560元 - {number = 4, name = (null)}
2018-09-26 16:45:14.320347+0800 TEST[12223:379270] 取20 还剩540元 - {number = 4, name = (null)}
2018-09-26 16:45:14.320459+0800 TEST[12223:379270] 取20 还剩520元 - {number = 4, name = (null)}
2018-09-26 16:45:14.320588+0800 TEST[12223:379270] 取20 还剩500元 - {number = 4, name = (null)}
2018-09-26 16:45:14.320693+0800 TEST[12223:379270] 取20 还剩480元 - {number = 4, name = (null)}
2018-09-26 16:45:14.320900+0800 TEST[12223:379270] 取20 还剩460元 - {number = 4, name = (null)}
2018-09-26 16:45:14.321222+0800 TEST[12223:379270] 取20 还剩440元 - {number = 4, name = (null)}
2018-09-26 16:45:14.321331+0800 TEST[12223:379270] 取20 还剩420元 - {number = 4, name = (null)}
2018-09-26 16:45:14.321548+0800 TEST[12223:379270] 取20 还剩400元 - {number = 4, name = (null)}
OSSpinLock目前已经不能使用的原因
OSSpinLock目前不建议使用的原因主要是会出现优先级反转。假设有3个线程线程1,线程2,线程3,那么如果这三个线程的优先级是一样的,那么CPU会平均的分配时间给这3个线程,比如首先给线程1 10ms去处理事件,然后给线程2 10ms去处理事件,再给线程3 10ms去处理事件,这样把时间切成碎片去处理,给人的感觉就像是三个线程一起在处理事件。但是当三个线程的优先级不一样的时候就会出现一些问题了,加入线程1的优先级较高,线程2的优先级较低,线程2首先访问资源,首先给资源加锁,这个时候线程1再去访问资源的时候,检查到锁已经加上了,所以就会在外面忙等,由于优先级很高,所以CPU分配给线程1的时间很多,分配给线程2的时间很少,这样会导致线程2没有时间来处理事件,锁很久不能打开,线程1长时间在外面等着,有点类似于死锁。
为了更加直管的观察各种锁,现在把存钱取钱卖票的业务逻辑抽到一个基类中,名为BaseDemo,主要代码如下:
@interface BaseDemo()
@property (nonatomic, assign)int money;
@property (nonatomic, assign)int ticketsCount;
@end
@implementation BaseDemo
- (void)moneyTest{
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//存钱的线程
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self saveMoney];
}
});
//取钱的线程
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self drawMoney];
}
});
}
//存钱
- (void)saveMoney{
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
//取钱
- (void)drawMoney{
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
- (void)saleTicket{
//这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
}
- (void)ticketTest{
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
@end
然后例如要演示OSSpinLock锁,我们可以创建一个类名为OSSPinLockDemo继承自BaseDemo,然后在其中实现存钱取钱卖票:
//OSSpinLockDemo.m
- (instancetype)init{
if (self = [super init]) {
self.moneyLock = OS_SPINLOCK_INIT;
self.ticketlock = OS_SPINLOCK_INIT;
}
return self;
}
- (void)saveMoney{
OSSpinLockLock(&_moneyLock);
[super saveMoney];
OSSpinLockUnlock(&_moneyLock);
}
- (void)drawMoney{
OSSpinLockLock(&_moneyLock);
[super drawMoney];
OSSpinLockUnlock(&_moneyLock);
}
- (void)saleTicket{
OSSpinLockLock(&_ticketlock);
[super saleTicket];
OSSpinLockUnlock(&_ticketlock);
}
在主函数中这样调用:
OSSpimLinkDemo *demo = [[OSSpimLinkDemo alloc] init];
[demo ticketTest];
这样做的好处是,我们可以更加专注于加锁的过程,而不用去管业务逻辑,每学习一个锁,就写一个子类。
os_unfair_lock
下面学习os_unfair_lock这种锁。
os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。
从底层调用看,等待os_unfair_lock锁的线程处于休眠状态,并非忙等。
需要导入头文件
os_unfair_lock的基本API如下:
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfair_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
接下来我们可以写一个子类OSUnFairLockDemo类,然后在这个类中重写卖票方法如下:
//OSUnFairLockDemo.m
- (instancetype)init{
if (self = [super init]) {
self.ticketlock = OS_UNFAIR_LOCK_INIT;
}
return self;
}
- (void)saleTicket{
os_unfair_lock_lock(&_ticketlock);
[super saleTicket];
os_unfair_lock_unlock(&_ticketlock);
}
然后看一下输出结果:
2018-09-27 16:06:24.453628+0800 TEST[26669:857080] 最后还剩的票数14 线程{number = 3, name = (null)}
2018-09-27 16:06:24.453777+0800 TEST[26669:857080] 最后还剩的票数13 线程{number = 3, name = (null)}
2018-09-27 16:06:24.453893+0800 TEST[26669:857080] 最后还剩的票数12 线程{number = 3, name = (null)}
2018-09-27 16:06:24.453988+0800 TEST[26669:857080] 最后还剩的票数11 线程{number = 3, name = (null)}
2018-09-27 16:06:24.454108+0800 TEST[26669:857080] 最后还剩的票数10 线程{number = 3, name = (null)}
2018-09-27 16:06:24.454235+0800 TEST[26669:857082] 最后还剩的票数9 线程{number = 4, name = (null)}
2018-09-27 16:06:24.454323+0800 TEST[26669:857082] 最后还剩的票数8 线程{number = 4, name = (null)}
2018-09-27 16:06:24.454421+0800 TEST[26669:857082] 最后还剩的票数7 线程{number = 4, name = (null)}
2018-09-27 16:06:24.454513+0800 TEST[26669:857082] 最后还剩的票数6 线程{number = 4, name = (null)}
2018-09-27 16:06:24.454600+0800 TEST[26669:857082] 最后还剩的票数5 线程{number = 4, name = (null)}
2018-09-27 16:06:24.454712+0800 TEST[26669:857083] 最后还剩的票数4 线程{number = 5, name = (null)}
2018-09-27 16:06:24.454840+0800 TEST[26669:857083] 最后还剩的票数3 线程{number = 5, name = (null)}
2018-09-27 16:06:24.458107+0800 TEST[26669:857083] 最后还剩的票数2 线程{number = 5, name = (null)}
2018-09-27 16:06:24.458217+0800 TEST[26669:857083] 最后还剩的票数1 线程{number = 5, name = (null)}
2018-09-27 16:06:24.458307+0800 TEST[26669:857083] 最后还剩的票数0 线程{number = 5, name = (null)}
可以看到,数据没有发生混乱。
pthread_mutex
mutex叫做"互斥锁",等待锁的线程会处于休眠状态。
需要导入头文件
与之相关的API有:
//初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
//初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
//尝试加锁
pthread_mutex_trylock(&mutex);
//加锁
pthread_mutex_lock(&mutex);
//解锁
pthread_mutex_unlock(&mutex);
//销毁相关资源
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
/*
*Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT
我们可以创建一个子类MutexDemo,然后重写卖票方法:
//MutexDemo.m
- (instancetype)init{
if (self = [super init]) {
//初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL );
//初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&_ticketLock, &attr);
pthread_mutexattr_destroy(&attr);
}
return self;
}
- (void)saleTicket{
pthread_mutex_lock(&_ticketLock);
[super saleTicket];
pthread_mutex_unlock(&_ticketLock);
}
打印出来数据没有发生混乱。
由一个问题引出递归锁
创建一个子类MutexDemo2,在这个类中像MutexDemo一样,创建pthread_Mutex类型的互斥锁:
- (instancetype)init{
if (self = [super init]) {
//初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
//通过属性确定创建的是互斥锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
//初始化锁
pthread_mutex_init(&_ticketLock, &attr);
pthread_mutexattr_destroy(&attr);
}
return self;
}
- (void)otherTest{
pthread_mutex_lock(&_ticketLock);
NSLog(@"%s", __func__);
[self otherTest2];
pthread_mutex_unlock(&_ticketLock);
}
- (void)otherTest2{
pthread_mutex_lock(&_ticketLock);
NSLog(@"%s", __func__);
pthread_mutex_unlock(&_ticketLock);
}
然后创建实例对象去调用otherTest这个方法:
MutexDemo2 *demo = [[MutexDemo2 alloc] init];
[demo otherTest];
我们看一下运行效果:
2018-09-27 18:44:56.627062+0800 TEST[30733:965088] -[MutexDemo2 otherTest]
只打印了otherTest方法中的输出,而没有打印otherTest2方法中的输出,这是什么原因呢?
原因在于,执行otherTest时,将ticketLock这个锁锁上了,锁上后去调用otherTest2方法,在otherTest2方法中,检查到锁锁上了,所以就会一直在碗面等,等这个锁打开,而锁打开又依赖于otherTest2方法执行完成,这样代码就没法执行下去了。
这个方法其实很好解决,由于是两个不同的方法,所以这两个方法使用不同的锁就行了,那么如果是递归呢?也就是otherTest里面调用otherTest呢?这样就不可能使用两把锁了,那这个问题又该怎么解决呢?
这个时候递归锁就派上用场了
递归锁:允许同一个线程对一把锁进行重复加锁
我们可以把pthread_Mutex锁的属性改为递归锁:
//改变锁的属性为递归锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
- (void)otherTest{
//第二次调用到这个地方的时候,可以再给ticketLock这个锁加一次锁
pthread_mutex_lock(&_ticketLock);
NSLog(@"%s", __func__);
[self otherTest];
//在解锁的时候相对应也会解两次锁
pthread_mutex_unlock(&_ticketLock);
}
这样就能解决这个递归死锁的问题。
从汇编实现来看自旋锁是忙等,互斥锁是休眠
我们在BaseDemo这个基类中修改ticketTest这个方法的实现,创建十条线程来调用saleTicket方法:
- (void)ticketTest{
self.ticketsCount = 15;
for (int i = 0; i < 15; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil] start];
}
然后在saleTicket这个方法里面设置睡眠时间为600s,这样一来,当第一条线程进入saleTicket方法后,由于休眠600s,所以锁在600s内会被锁着,当第二条线程调用saleTicket方法时,就会在外面等待:
- (void)saleTicket{
//睡眠600s是保证第二条线程进来时锁是被锁着,于是w要在外面等待
int oldTicketsCount = self.ticketsCount;
sleep(600);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
}
为了研究自旋锁,我们选择OSSpinLock这个锁,在OSSpinLock的类文件中打下断点:
当第一条线程访问时,直接过掉断点,第二条线程执行到断点处时,进入汇编里面查看等待的过程。
下面是第二条线程执行到断点处时进入汇编:
我们可以使用stepi指令或者si指令来一步一步执行汇编指令,这样单步执行遇到函数时会调进函数。
然后我们使用si指令来一步一步执行汇编指令,执行到ox105f329b1时跳进去了,通过si一步一步的执行,最终来到了下面的汇编:
执行的时候发现,汇编指令在0x107ef3a32和0x107ef3a43之间循环执行,jne就是一个while循环,条件满足就继续执行框内的代码,等待条件不满足也就是锁已经打开就继续往下执行。 这里也就证明了自旋锁使用的是忙等。
为了研究互斥锁,我们选择pthread_Mutex这个锁,单步执行很多次之后,跳到了下图:
采用研究OSSpinLock一样的方法,通过汇编指令来解读
这个syscall是一个系统级的函数,单步执行到这一步的时候,下一步就是执行这个函数了,执行这一步之后,马上退出了汇编指令的界面,回到了模拟器的界面。这就说明线程产生了休眠,不干事了,所以会退出。 这也就说明了互斥锁在等待的时候会线程休眠。
通过汇编指令判断os_unfair_lock是自旋锁还是互斥锁
还是通过和前面两个锁一样的方法来查看,单步执行汇编指令,执行到最后到了下面的指令:
汇编指令执行到最后还是执行到了syscall这一步,这就说明os_unfair_lock在等待时线程是休眠的,也就证明了其是互斥锁。
NSLock
NSLock是对mutex普通锁的封装,所以它是一种互斥锁。
@interface NSLock : NSObject {
- (BOOL)tryLock;
//在这个时间之前如果能等到这把锁放开,那么就给这把锁加锁,加锁成功,返回YES,如果到了规定的时间这把锁还是没有放开,那就加锁失败,返回NO。
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
其遵循的NSLocking协议如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
因此NSLock使用起来也是非常简单,创建:
NSLock *lock = [[NSLock alloc] init];
上锁:
[lock lock];
解锁:
[lock unlock];
NSRecursiveLock递归锁
这个锁是对mutex递归锁的封装,也就是mutex锁的属性为PTHREAD_MUTEX_RECURSIVE
,这就是NSRecursiveLock锁了,这个锁的API和NSLock基本一致:
@interface NSRecursiveLock : NSObject {
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
其同样遵守NSLocking协议。在使用上与NSLock也是基本一致。
NSCondition
NSCondition是对mutex和cond的封装
其主要API如下:
@interface NSCondition : NSObject {
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@end
下面举一个例子说明其应用:
有两条线程,一条线程对数组元素进行删除操作,一条进行添加操作。这个时候在做删除操作的时候就要格外小心,因为如果数组为空,进行删除操作就可能引发崩溃,这个时候就可以在删除操作中做个判断,如果元素数为0,那么就等待,线程进入休眠状态。同时,在添加元素的操作中也要做处理,当添加完元素后要发出一个信号,这个信号告诉删除的那条线程可以醒来继续处理了。
@interface NSConditionDemo()
@property (nonatomic, strong)NSMutableArray *data;
@property (nonatomic, strong)NSCondition *condition;
@end
@implementation NSConditionDemo
- (instancetype)init{
if (self = [super init]) {
self.data = [[NSMutableArray alloc] init];
self.condition = [[NSCondition alloc] init];
}
return self;
}
- (void)__remove{
[self.condition lock];
NSLog(@"__rermove - begin");
if (self.data.count == 0) {
[self.condition wait];
}
[self.data removeLastObject];
NSLog(@"删除了元素");
[self.condition unlock];
}
- (void)__add{
[self.condition lock];
sleep(1.0);
[self.data addObject:@"test"];
[self.condition signal];
NSLog(@"添加了元素");
[self.condition unlock];
}
- (void)otherTest{
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}
@end
首先调用的是remove操作,进入remove后先加锁,然后判断元素个数是否为0,如果是0那就让线程进入休眠,同时放开锁。然后执行add操作,进入add操作后马上加锁,当添加元素完成后就发出信号,这时remove那条线程就会被唤醒,但是由于add操作时加的锁还没有放开,所以remove线程还要等待锁放开才能继续执行,当锁放开后就能执行删除元素的操作了,完成之后就把锁放开。
dispatch_semaphore
semaphore叫做"信号量"
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
相关API如下:
//信号量的初始值
int value = 1;
//初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
//如果c信号量的值<=0,当前线程就会进入休眠等待(直到信号量的值>0)
//如果信号量的值>0,就减1,然后往下执行后面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//让信号量的值加1
dispatch_semaphore_signal(semaphore);
下面以一个实例来讲解信号量的用法:
要创建20条线程,每条线程执行同样的方法,这样20条线程会对同样的代码执行同样的方法,现在要限制同时执行该方法的线程数为5,那么 就可以使用信号量:
@interface SemaphoreDemo()
@property (strong ,nonatomic)dispatch_semaphore_t sempahore;
@end
@implementation SemaphoreDemo
- (instancetype)init{
if (self = [super init]) {
self.sempahore = dispatch_semaphore_create(5);
}
return self;
}
- (void)otherTest{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
- (void)test{
dispatch_semaphore_wait(_sempahore, DISPATCH_TIME_FOREVER);
//这是为了使效果更明显
sleep(1);
NSLog(@"test - %@", [NSThread currentThread]);
dispatch_semaphore_signal(_sempahore);
}
@end
第一条线程执行test方法时信号量的值是5,在dispatch_semaphore_wait()这里,当信号量>0时会让线程进入,然后信号量减1,当信号量=0时就会让线程在外面等待,直到信号量>0才让线程进入。进入的线程在执行完以后会进入dispatch_semaphore_signal(),这个方法让信号量加1。
如果要用信号量保证线程同步,只需要使最大并发线程数为1。
NSConditionLock
NSConditionlock是对NSCondition的进一步封装,可以设置具体的条件值
具体的API如下:
@interface NSConditionLock : NSObject {
- (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;
@end
比如我有三个任务,任务1,任务2,任务3,我想要让任务1完成后再执行任务2,任务2执行完后再执行任务3,那么这时就可以使用条件锁:
@interface NSConditionLockDemo()
@property (nonatomic, strong)NSConditionLock *conditionLock;
@end
@implementation NSConditionLockDemo
- (instancetype)init{
if (self = [super init]) {
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}
return self;
}
- (void)task1{
//当这把锁内部所存储的条件值为1的时候就会进行加锁,否则就会在这里等待
[self.conditionLock lockWhenCondition:1];
NSLog(@"任务一");
//设置这把锁内部的条件值为2,同时把锁放开
[self.conditionLock unlockWithCondition:2];
}
- (void)task2{
//当条件值为2且锁放开时加锁
[self.conditionLock lockWhenCondition:2];
NSLog(@"任务二");
//设置这把锁内部的i条件值为3,同时把锁放开
[self.conditionLock unlockWithCondition:3];
}
- (void)task3{
//当条件值为3且锁放开时加锁
[self.conditionLock lockWhenCondition:3];
NSLog(@"任务三");
[self.conditionLock unlock];
}
- (void)otherTest{
[[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start];
}
@end
SerialQueue
线程同步的本质是不能让多条线程占用同一份资源,直接使用GCD的串行队列,也可以实现线程同步
例如卖票的方法,要让票一张一张的卖,那也可以使用串行队列,把卖票的方法加入串行队列中,这样就能实现一张票卖完了之后才开始卖下一张票。
@synchronized
@synchronized是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁,加锁操作
从代码简洁度来看是最简单的方案
在买票的程序里我们可以这样用@synchronized:
- (void)saleTicket{
static NSObject *lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSObject alloc] init];
});
//保证每次传入的是同一个对象
@synchronized (lock) {
[super saleTicket];
}
}
@synchronized的括号里相当于就是一把锁,这就相当于是给括号里的一把锁上锁,大括号里就是要执行的东西。任何对象都可以传入括号里面当锁,但是为了让大括号内的代码同一时刻只能被执行一次,这就要求每个线程进来时用的锁是一样的,所以这里声明了一个static类型的NSObject对象,并用单例去创建它。
多线程同步方案性能对比
性能由高到低排序:
什么情况下使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算? - 预计线程等待时间较长
- 单核处理器
- 加锁的代码有IO操作(耗性能)
atomic
我们都知道,属性修饰符中有nonatomic和atomic,但是我们在申明属性的时候好像用的都是nonatomic而不是atomic,这是为什么呢?atomic又是什么意思呢?
atomic用于保证属性setter,getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,会进行加锁和解锁。
可以参考runtime源码的objc-accessors.mm文件。
它并不能保证使用属性的过程是线程安全的。
当我们声明一个属性的时候,系统会自动帮我们实现set和get方法,比如我们声明一个NSString类型的name属性,并用nonatomic来修饰,那么其set和get方法的默认实现如下:
- (NSString *)name{
return _name;
}
- (void)setName:(NSString *)name{
_name = name;
}
上面是用nonatomic方法修饰属性,如果是用atomic修饰属性,那么就会在访问属性和设置属性的时候给其加上锁:
//保证内部的线程同步
- (NSString *)name{
//加锁
return _name;
//解锁
}
- (void)setName:(NSString *)name{
//加锁
_name = name;
//解锁
}
下面我们通过源码来证实一下:
打开runtime源码的objc-accessors.mm文件,先看取值方法:
再看一下设值的方法:
使用atomic确实可以保证set方法和get方法内部是线程安全的,但是它并不能保证使用属性的过程是线程安全的,这句话是什么意思呢?
比如说有一个data属性:
@property (atomic, strong)NSMutableArray *data;
那么下列代码是不是线程安全的呢:
self.data = [[NSMutableArray alloc] init];
[self.data addObject:@"1"];
[self.data addObject:@"2"];
[self.data addObject:@"3"];
有人可能会想,这不就是取值和设值的操作吗?就是调用了set和get方法呀,而atomic修饰的属性,其set和get方法是线程安全的呀。上述代码可以等价于下面的:
[self setData:[[NSMutableArray alloc] init]];
[[self data] addObject:@"1"];
[[self data] addObject:@"2"];
[[self data] addObject:@"3"];
问题出就出在,并不是只用了set和get方法,还有addObject方法呀,这可不是线程安全的,加入有多条线程同时执行addObject方法,它就不是安全的了。
由于set方法和get方法使用的非常多,而如果是用atomic修饰的话,那么每使用一次set或者get方法都会进行加锁和解锁,这样频繁的加锁和解锁是非常耗性能的,并且也不能保证使用属性的过程是线程安全的,因此一般不用atomic,转而用nonatomic。