1. 什么情况下会有线程隐患?
我们在使用多线程技术带来的便利的同时,也需要考虑下多线程所带来的隐患。比如,我们可以并发
的进行多个任务,因此同一块资源就可能在多个线程中同时被访问(读/写)
,这个现象叫做资源共享
,比如多个线程同时访问了同一个对象\变量\文件,这样就有可能引发数据错乱
和数据安全
问题。
2. 两个常见的示例
2.1 示例:存钱取钱
我们用代码来实现一下这个功能:
- (void)moneyTest{
self.money = 1000;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 10; i++) {
[self drawMoney];
}
});
}
// 存钱
- (void)saveMoney{
NSInteger oldMoney = self.money;
sleep(0.5);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存了50元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
}
// 取钱
- (void)drawMoney{
NSInteger oldMoney = self.money;
sleep(0.5);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取了20元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
}
输出结果:
2019-10-12 16:40:18.033330+0800 tianran[16615:4423457] 存了50元,账户余额 1050,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033486+0800 tianran[16615:4423457] 存了50元,账户余额 1080,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033500+0800 tianran[16615:4423456] 取了20元,账户余额 1030,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033573+0800 tianran[16615:4423457] 存了50元,账户余额 1130,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033632+0800 tianran[16615:4423456] 取了20元,账户余额 1110,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033659+0800 tianran[16615:4423457] 存了50元,账户余额 1160,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033730+0800 tianran[16615:4423456] 取了20元,账户余额 1140,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033737+0800 tianran[16615:4423457] 存了50元,账户余额 1190,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033821+0800 tianran[16615:4423457] 存了50元,账户余额 1240,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033839+0800 tianran[16615:4423456] 取了20元,账户余额 1170,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033949+0800 tianran[16615:4423457] 存了50元,账户余额 1220,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034040+0800 tianran[16615:4423456] 取了20元,账户余额 1200,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034178+0800 tianran[16615:4423457] 存了50元,账户余额 1250,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034341+0800 tianran[16615:4423456] 取了20元,账户余额 1230,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034423+0800 tianran[16615:4423457] 存了50元,账户余额 1280,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034581+0800 tianran[16615:4423456] 取了20元,账户余额 1260,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034665+0800 tianran[16615:4423457] 存了50元,账户余额 1310,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034803+0800 tianran[16615:4423456] 取了20元,账户余额 1290,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.035029+0800 tianran[16615:4423456] 取了20元,账户余额 1270,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.035105+0800 tianran[16615:4423456] 取了20元,账户余额 1250,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
我们在moneyTest
方法中,以多线程方式分别进行了10次
的存/取钱操作,每次存50
,每次取20
,最后执行完之后余额应该为1000 + (50 * 10) - (20 * 10) = 1300
,但是上面输出的结果是1250
,很明显,出大问题了。
2.2 示例:卖票问题
我们通过代码展示一下这个问题:
- (void)ticketTest{
self.ticketNum = 30;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
// 卖票操作
- (void)saleTicket{
self.ticketNum -= 1;
NSLog(@"还剩%ld张票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
}
输出结果:
2019-10-12 16:46:38.501449+0800 tianran[16620:4425096] 还剩29张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501573+0800 tianran[16620:4425094] 还剩28张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501593+0800 tianran[16620:4425096] 还剩27张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501684+0800 tianran[16620:4425096] 还剩25张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501693+0800 tianran[16620:4425094] 还剩24张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501689+0800 tianran[16620:4425093] 还剩26张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.501753+0800 tianran[16620:4425096] 还剩23张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501776+0800 tianran[16620:4425094] 还剩22张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501781+0800 tianran[16620:4425093] 还剩21张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.501877+0800 tianran[16620:4425094] 还剩19张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501819+0800 tianran[16620:4425096] 还剩20张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.502044+0800 tianran[16620:4425093] 还剩18张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.502404+0800 tianran[16620:4425094] 还剩17张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.502529+0800 tianran[16620:4425093] 还剩16张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.502760+0800 tianran[16620:4425093] 还剩15张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
虽然最后的结果是一样的,但是我们可以看到中间卖票的时候,票数有问题了。
上面两个问题都是由于多个线程对同一资源
进行了读写操作
而导致的,下面用一个熟悉的图片来表示下:
针对这个问题的解决方案:使用线程同步
技术(同步,就是协同步调,按预定的先后次序进行)。常见的线程同步技术就是:加锁
。
如下图所示:
在进行操作的时候,先加锁,保证此时只有一个线程对资源进行操作,操作完成后在解锁。
3. 线程同步(加锁)方案
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- @synchronized
借鉴一张锁的性能数据对比图,如下所示:
介绍锁之前,我们可以先看几个概念定义:
临界区:
指的是一块对公共资源
进行访问的代码,并非一种机制或算法。自旋锁:
用于多线程同步的一种锁,线程反复检查锁变量是否可用
。由于线程在这一过程中保持执行,因此是一种忙等待
。一旦获取了自旋锁,线程会一直保持该锁,直至显式的释放
自旋锁。自旋锁避免了线程上下文的调度开销
,因此对于线程只会阻塞很短时间的场景
是非常有效的,如上面的OSSpinLock
和atomic
。互斥锁(
Mutex
):
是一种用于多线程编程中,防止多个线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区
而达成,如上面的@ synchronized、NSLock 、pthread_mutex
。条件锁
条件锁就是条件变量
,当进程的某些资源要求不满足时就进入休眠
,即锁住了,当资源被分配到了,条件锁打开了,进程继续运行,如上面的NSCondition
、NSConditionLock
递归锁
递归锁就是同一个线程可以加锁 N 次而不会引发死锁。递归锁是特殊的互斥锁,即带有递归性质的互斥锁,如:pthread_mutex(recursive)
、NSRecursiveLock
。读写锁:
是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”或者“多读-单写锁”,是一种特殊的自旋锁
。用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。信号量(semaphore):
是一种更高级的同步机制
,互斥锁
可以说是semaphore
在仅取值0/1
时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥,如:dispatch_semaphore
。
其实基本的锁就包括三类:自旋锁、互斥锁、读写锁,其他的比如条件锁、递归锁、信号量都是上层的封装和实现。
3.1 OSSpinLock
OSSpinLock叫做“自旋锁”,需要导入头文件
#import <libkern/OSAtomic.h>
常用的API:
OSSpinLock lock = OS_UNFAIR_LOCK_INIT; ——初始化锁对象lock
OSSpinLockTry(&lock);——尝试加锁,加锁成功继续,加锁失败返回,继续执行后面的代码,不阻塞线程
OSSpinLockLock(&lock);——加锁,加锁失败会阻塞线程,进行等待
OSSpinLockUnlock(&lock);——解锁
我们使用上面的第二个卖票示例来进行加锁操作:
#import <libkern/OSAtomic.h>
@interface ViewController ()
// 票总数
@property (nonatomic, assign) NSInteger ticketNum;
// 锁
@property (nonatomic, assign) OSSpinLock lock;
@end
- (void)ticketTest{
// 初始化锁
self.lock = OS_SPINLOCK_INIT;
self.ticketNum = 30;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
// 卖票操作
- (void)saleTicket{
// 加锁🔐🔐
OSSpinLockLock(&_lock);
self.ticketNum -= 1;
NSLog(@"还剩%ld张票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
// 解锁🔓🔓
OSSpinLockUnlock(&_lock);
}
结果输出:
2019-10-12 17:02:55.774748+0800 tianran[16628:4429441] 还剩29张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.774955+0800 tianran[16628:4429445] 还剩28张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775065+0800 tianran[16628:4429445] 还剩27张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775156+0800 tianran[16628:4429443] 还剩26张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775237+0800 tianran[16628:4429443] 还剩25张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775301+0800 tianran[16628:4429443] 还剩24张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775368+0800 tianran[16628:4429443] 还剩23张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775433+0800 tianran[16628:4429443] 还剩22张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775513+0800 tianran[16628:4429445] 还剩21张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775578+0800 tianran[16628:4429445] 还剩20张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775643+0800 tianran[16628:4429445] 还剩19张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775717+0800 tianran[16628:4429441] 还剩18张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775781+0800 tianran[16628:4429441] 还剩17张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775911+0800 tianran[16628:4429441] 还剩16张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775983+0800 tianran[16628:4429441] 还剩15张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
这里要注意,我们使用的是同一个锁对象
,如果用多个,肯定也没效果啊。
卖票的问题,我们针对的是同一个操作来处理的,而存钱取钱的问题,涉及到了两个操作(存钱和取钱),首先要明确问题,加锁机制是为了解决多个线程同时访问共享资源
所产生的数据问题,无论这些线程里面执行的是什么操作,如果这些操作影响了共享资源,不能同时进行的话,那么应该对这些操作使用同一个锁对象进行加锁。
所以,我们应该对存钱操作
和取钱操作
使用相同的锁对象
进行加锁。
代码如下:
#import <libkern/OSAtomic.h>
@interface ViewController ()
// 总钱数
@property (nonatomic, assign) NSInteger money;
// 锁
@property (nonatomic, assign) OSSpinLock lock;
@end
- (void)moneyTest{
// 初始化锁
self.lock = OS_SPINLOCK_INIT;
self.money = 1000;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 10; i++) {
[self drawMoney];
}
});
}
// 存钱
- (void)saveMoney{
sleep(0.5);
OSSpinLockLock(&_lock);
NSInteger oldMoney = self.money;
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存了50元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
OSSpinLockUnlock(&_lock);
}
// 取钱
- (void)drawMoney{
sleep(0.5);
OSSpinLockLock(&_lock);
NSInteger oldMoney = self.money;
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取了20元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
OSSpinLockUnlock(&_lock);
}
输出结果:
2019-10-12 17:14:44.362398+0800 tianran[16653:4433835] 存了50元,账户余额 1050,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362540+0800 tianran[16653:4433832] 取了20元,账户余额 1030,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.362640+0800 tianran[16653:4433835] 存了50元,账户余额 1080,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362711+0800 tianran[16653:4433832] 取了20元,账户余额 1060,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.362790+0800 tianran[16653:4433835] 存了50元,账户余额 1110,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362923+0800 tianran[16653:4433832] 取了20元,账户余额 1090,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363015+0800 tianran[16653:4433835] 存了50元,账户余额 1140,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363078+0800 tianran[16653:4433832] 取了20元,账户余额 1120,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363152+0800 tianran[16653:4433835] 存了50元,账户余额 1170,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363206+0800 tianran[16653:4433832] 取了20元,账户余额 1150,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363278+0800 tianran[16653:4433835] 存了50元,账户余额 1200,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363335+0800 tianran[16653:4433832] 取了20元,账户余额 1180,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363405+0800 tianran[16653:4433835] 存了50元,账户余额 1230,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363458+0800 tianran[16653:4433832] 取了20元,账户余额 1210,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363555+0800 tianran[16653:4433835] 存了50元,账户余额 1260,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363723+0800 tianran[16653:4433835] 存了50元,账户余额 1310,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363770+0800 tianran[16653:4433835] 存了50元,账户余额 1360,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363889+0800 tianran[16653:4433832] 取了20元,账户余额 1340,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.364014+0800 tianran[16653:4433832] 取了20元,账户余额 1320,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.364129+0800 tianran[16653:4433832] 取了20元,账户余额 1300,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
这次就没问题了,过程和结果都没有问题。
上面有提到,OSSpinLock
是自旋锁
,那么为什么叫做自旋锁呢?
自旋锁的原理是当已经被别的线程加锁了,加锁失败的时候,让线程处于忙等
的状态,以此让线程停留在临界区
(需要加锁的代码段)之外,一旦加锁成功,线程便可以进入临界区进行对共享资源操作。
让线程阻塞有两种方法:
- 让线程休眠,
RunLoop
里面用到的mach_msg()
实现的效果就是这一种,它借助系统内核指令,🙆线程停下来,CPU不再分配资源给线程,因此不会再执行任何一句汇编指令,我们后面要介绍的互斥所,也是属于这种,它的底层汇编调用了一个系统函数syscall
使得线程进入休眠 - 自旋锁的忙等,本质上是一个
while循环
,不断的去判断加锁条件,一旦当前已经进入临界区(加锁代码块)的线程完成了操作,解开锁之后,等待锁的线程便可以成功加锁,再次进入临界区。自旋锁其实并没有真正让线程停下来,线程只不过是暂时被困在while循环里面,CPU还是在不断的分配资源去处理它的汇编指令的(while循环的汇编指令)。
现在苹果已经建议开发者停止使用OSSpinLock
了,在线程优先级的作用下,会产生【优先级反转】
,使得自旋锁卡住,因此它不再安全了。
可以看不再安全的OSSpinLock。
我们知道,计算机的CPU
在同一时间,只能处理一条线程,对于单核``CPU
来说,线程的并发,实际上是一种假象,是系统让CPU
一很小的时间间隔在线程之间来回切换
,所以看上去多条线程好像是在同时进行的。到了多核CPU
时代,确实可以实现真正的线程并发,但是CPU核心数
毕竟是有限的,而程序内部的线程数量通常肯定是远大于CPU数量
的,因此,很多情况下我们面对的还是单CPU处理多线程的情况。基于这种场景,需要了解一个概念叫做线程优先级
,CPU会将尽可能多的时间(资源)
分配给优先级高的线程
,我们用下图来展示一下所谓的优先级反转问题:
低优先级
的线程A
获得锁并访问共享资源,这时一个高优先级
的线程B
也尝试获得这个锁,它会处于spin lock的忙等状态
从而占用大量CPU,此时低优先级的线程A
无法与高优先级线程B
争夺CPU时间,从而导致线程A
任务迟迟完不成,无法释放lock
,所以会出现卡住的问题。
自旋锁的while循环
本质,使得线程并没有停下来,一般情况下,一条线程等待锁的时间不会太长,选用自旋锁来阻塞线程所消耗的CPU资源,要小于线程的休眠和唤醒所带来的CPU资源开销,因此自选锁是一种效率很高
的加锁机制,但是优先级反转问题
使得自旋锁不再安全,锁的最终目的是安全不是效率,所以苹果放弃了OSSpinLock
。
在iOS 10/macOS 10.12
发布时,苹果提供了新的 os_unfair_lock
作为 OSSpinLock
的替代,并且将OSSpinLock
标记为了 Deprecated
。
另外为什么RunLoop
要选择真正的线程休眠
呢?因为对于App来说,可能处于长时间的搁置状态,而没有任何用户行为发生,不需要CPU管,对于这种场景,当然是让线程休眠更为节约性能。
3.2 os_unfair_lock
苹果建议从iOS10.0
之后,使用os_unfair_lock
代替OSSpinLock
,现在我们就去看一下如何使用:
先导入头文件
#import <os/lock.h>
使用的API:
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
初始化锁对象lock
os_unfair_lock_trylock(&lock);
尝试加锁,加锁成功继续,加锁失败返回,继续执行后面的代码,不阻塞线程
os_unfair_lock_lock(&lock);
加锁,加锁失败会阻塞线程进行等待
os_unfair_lock_unlock(&lock);
解锁
它的使用和OSSpinLock
的方法一样,这里就不再举例了,苹果为了解决OSSpinLock
的优先级反转问题,在os_unfair_lock
中摒弃了忙等方式,使用让线程真正休眠
的方式,来阻塞线程,也就从根本上解决了问题。
3.3 atomic
atomic
适用于OC
中属性的修饰符,其自带一把互斥锁
,但是这个一般基本不使用,都是使用的nonatomic
。
setter
方法会根据修饰符调用不同的方法,其中最后会统一调用 reallySetProperty
方法,其中就有对于atomic
和非 atomic
的操作:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
...
id *slot = (id*) ((char*)self + offset);
...
if (!atomic) {//未加锁
oldValue = *slot;
*slot = newValue;
} else {//加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
...
}
从源码中可以看出,对于atomic修饰
的属性,进行了spinlock_t
加锁处理,但是在前文中提到OSSpinLock
已经废弃了,这里的spinlock_t
在底层是通过os_unfair_lock
互斥锁替代了OSSpinLock
自旋锁:
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
...
}
3.4 pthread_mutex
pthread_mutex
来自pthread
,是一个跨平台的解决方案,mutex
意为互斥锁,等待锁的线程会处于休眠状态,它有如下API:
需要先导入 #import <pthread.h>
初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NOMAL);
初始化锁
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(&attr);
我们先介绍一下pthread_mutex
的初始化方法:
int pthread_mutex_init(pthread_mutex_t * __restrict,
const pthread_mutexattr_t * _Nullable __restrict);
-
pthread_mutex_t * __restrict
要进行初始化的锁对象 -
const pthread_mutexattr_t * _Nullable __restrict
锁对象的属性
由于第二个参数是锁对象的属性,所以我们还需要专门生成属性对象,通过 定义属性对象
-> 初始化属性对象
-> 设置属性种类
3步来完成,属性的类别有以下几类:
#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_init(mutex, NULL);
好了,我们先用卖票示例来使用一下pthread_mutex
,代码如下:
#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketNum;
@property (nonatomic, assign) pthread_mutex_t pthread_lock;
@end
- (void)ticketTest{
// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_init(&_pthread_lock, &attr);
self.ticketNum = 30;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
// 卖票操作
- (void)saleTicket{
// 加锁🔐🔐
pthread_mutex_lock(&_pthread_lock);
self.ticketNum -= 1;
NSLog(@"还剩%ld张票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
// 解锁🔓🔓
pthread_mutex_unlock(&_pthread_lock);
}
-(void)dealloc{
pthread_mutex_destroy(&_pthread_lock);
}
输出结果就不贴了,是没有问题的,太占地方了。
3.5 互斥递归锁
请看下面的代码场景:
-(void)otherTest {
NSLog(@"%s",__func__);
[self otherTest2];
}
-(void)otherTest2 {
NSLog(@"%s",__func__);
}
如果正常调用 otherTest 这个方法,结果如下:
[ViewController otherTest]
[ViewController otherTest2]
如果这两段代码都需要保证线程安全,我们通过加互斥锁,来看下效果:
-(void)otherTest {
//初始化属性
pthread_mutexattr_init(&_attr);
pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
//初始化锁
pthread_mutex_init(&_mutex, &_attr);
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);
}
输出结果:
-[ViewController otherTest]
像上面的代码一样,两个方法都加上同一把锁,可以看到调用otherTest
方法会导致线程卡在该方法里面,只完成了打印代码的执行,就不继续往下走了,为啥呢?看下图:
我们可以从上面代码中看到,在otherTest方法中,先加锁了, [self otherTest2] 在加锁之后调用,但是otherTest2方法中,此时还是加锁的状态,所以otherTest2方法一直不执行,由于otherTest方法中,解锁操作是在otherTest2方法执行之后,所以造成死锁。
要解决这个问题很简单,我们分别给两个方法加上不同的锁对象就可以解决了。
但是,如果在开发中如果碰到需要给 递归函数 加锁,如下面所示:
-(void)otherTest {
pthread_mutex_lock(&_mutex);
NSLog(@"%s",__func__);
//业务逻辑
[self otherTest];
pthread_mutex_unlock(&_mutex);
}
这样就无法通过不同的锁对象来加锁了,只要使用相同的锁对象,就会出现死锁。针对这种情况,ptherad给我们提供了递归锁来解决这个问题。要想使用递归锁,我们只需要在初始化属性的时候,选择递归锁属性即可,其他的使用步骤跟普通互斥锁没有区别,如下:
pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_RECURSIVE);
那么递归锁是如何避免死锁的呢?对于同一个锁对象来说,允许重复的加锁,重复的解锁,因为对于一个有出口的递归函数来说:
函数的调用次数 = 函数的退出次数
因此加锁次数pthread_mutex_lock和解锁的次数pthread_mutex_unlock是相等的,所以递归函数结束时,所有的锁都会被解开。
但是递归锁只是针对在同一个线程里面可以重复加锁和解锁。
3.6 互斥条件锁 pthread_cond_t
pthread_mutex_t mutex; 定义一个锁对象
pthread_mutex_init(&mutex, NULL); 初始化锁对象
pthread_cond_t condition; 定义一个条件对象
pthread_cond_init(&condition, NULL); 初始化条件对象
pthread_cond_wait(&condition, &mutex); 等待条件
pthread_cond_signal(&condition); 激活一个等待该条件的线程
pthread_cond_broadcast(&condition); 激活所有等待条件的线程
pthread_mutex_destroy(&mutex); 销毁锁对象
pthread_cond_destroy(&condition); 销毁条件对象
为了解释互斥锁条件的作用,我们来设计一种场景案例:
- 在 remove 方法里对数组 dataArr 进行删除元素操作
- 在 add 方法里面对dataArr 进行元素添加操作
- 并且要求,如果 dataArr 的元素个数为0,则不能进行删除操作
代码如下:
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *dataArr;
// 锁对象
@property (nonatomic, assign) pthread_mutex_t pthread_lock;
// 条件对象
@property (nonatomic, assign) pthread_cond_t cond;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化属性
pthread_mutexattr_t _attr;
pthread_mutexattr_init(&_attr);
pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_init(&_pthread_lock, &_attr);
// 初始化条件
pthread_cond_init(&_cond, NULL);
pthread_mutexattr_destroy(&_attr);
// 初始化数组
self.dataArr = [NSMutableArray array];
[self dataArrTest];
}
- (void)dataArrTest{
// 开启remove 操作,此时dataArr为空
NSThread *removeThread = [[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil];
[removeThread setName:@"RemoveThread"];
[removeThread start];
// 开启add操作
NSThread *addThread = [[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil];
[addThread setName:@"AddThread"];
[addThread start];
}
// 移除元素
- (void)remove{
// 加锁
pthread_mutex_lock(&_pthread_lock);
NSLog(@"加锁成功,线程:%@,remove操作开始",[NSThread currentThread]);
if (!self.dataArr.count) {
// 进行条件等待
NSLog(@"dataArr没有元素,开始等待");
pthread_cond_wait(&_cond, &_pthread_lock);
NSLog(@"接收到条件更新信号,dataArr有了元素,继续remove操作");
}
[self.dataArr removeLastObject];
NSLog(@"remove成功,dataArr剩余元素个数:%ld",self.dataArr.count);
// 解锁
pthread_mutex_unlock(&_pthread_lock);
NSLog(@"remove 解锁成功,线程:%@,线程结束\n",[NSThread currentThread]);
}
// 添加元素
- (void)add{
// 加锁
pthread_mutex_lock(&_pthread_lock);
NSLog(@"加锁成功,线程:%@,add操作开始",[NSThread currentThread]);
sleep(2);
[self.dataArr addObject:@"111"];
NSLog(@"add成功,dataArr剩余元素个数:%ld,发送条件信号",self.dataArr.count);
// 发送条件信号
pthread_cond_signal(&_cond);
// 解锁
pthread_mutex_unlock(&_pthread_lock);
NSLog(@"add 解锁成功,线程:%@,线程结束",[NSThread currentThread]);
}
-(void)dealloc{
pthread_mutex_destroy(&_pthread_lock);
}
@end
输出结果:
2019-10-14 15:14:52.929491+0800 tianran[2004:431723] 加锁成功,线程:<NSThread: 0x283f8cd40>{number = 6, name = RemoveThread},remove操作开始
2019-10-14 15:14:52.929557+0800 tianran[2004:431723] dataArr没有元素,开始等待
2019-10-14 15:14:52.929641+0800 tianran[2004:431724] 加锁成功,线程:<NSThread: 0x283f8c9c0>{number = 7, name = AddThread},add操作开始
2019-10-14 15:14:54.935014+0800 tianran[2004:431724] add成功,dataArr剩余元素个数:1,发送条件信号
2019-10-14 15:14:54.935672+0800 tianran[2004:431723] 接收到条件更新信号,dataArr有了元素,继续remove操作
2019-10-14 15:14:54.935790+0800 tianran[2004:431724] add 解锁成功,线程:<NSThread: 0x283f8c9c0>{number = 7, name = AddThread},线程结束
2019-10-14 15:14:54.936061+0800 tianran[2004:431723] remove成功,dataArr剩余元素个数:0
2019-10-14 15:14:54.936415+0800 tianran[2004:431723] remove 解锁成功,线程:<NSThread: 0x283f8cd40>{number = 6, name = RemoveThread},线程结束
从案例以及运行结果分析,互斥锁的条件pthread_cond_t可以在线程加锁之后,如果条件不达标,暂停线程,等到条件符合标准,继续执行线程,请看下图:
总结一下pthread_cond_t的作用:
- 首先在A线程内碰到业务逻辑无法往下执行的时候,调用pthread_cond_wait(),这句代码首先会解锁当前线程,然后休眠当前线程以等待条件信号
- 此时,锁已经解开,那么值钱等待锁的B线程可以成功加锁,执行它后面的逻辑,由于B线程内的某些操作完成后可以出发A的运行条件,此时从B线程通过pthread_cond_signal(&_cond)向外发出条件信号
- A线程的收到了条件信号就会被pthread_cond_t唤醒,一旦B线程解锁之后,pthread_cond_t会在A线程内重新加锁,继续A线程的后续操作,并最终解锁。从前到后,有三次加锁,三次解锁
- 通过pthread_cond_t就实现了一种线程与线程之间的依赖关系,实际开发中我们会有不少场景需要用到这种跨线程依赖关系
3.7 NSLock NSRecursiveLock NSCondition
上面我们了解的 mutex普通锁
,mutex递归锁
,mutex条件锁
,都是基于C语言的API,苹果在此基础上,进行了一层面向对象的封装:
-
NSLock
封装了pthread_mutex_t
(attr = 普通) -
NSRecursiveLock
封装了pthread_mutex_t
(attr = 递归) -
NSCondition
封装了pthread_mutex_t + pthread_cond_t
由于底层是pthread_mutex
,这里不再通过代码案例演示,下面列举一下相关API使用方法:
//普通锁
NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];
//递归锁
NSRecursiveLock *rec_lock = [[NSRecursiveLock alloc]
[rec_lock lock];
[rec_lock unlock];init];
//条件锁
NSCondition *condition = [[NSCondition alloc] init];
[self.condition lock];
[self.condition wait];
[self.condition signal];
[self.condition unlock];
3.8 NSConditionLock
苹果基于NSCondition
又进一步封装了NSConditionLock
,该锁允许我们在锁中设定条件具体条件值,有了这个功能,我们可以更加方便的管理多条线程的依赖关系和前后执行顺序,首先看一下相关API:
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
特色功能:
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
接下来通过案例来说明它的功能:
@interface ViewController ()
@property (nonatomic, strong) NSConditionLock *conditionLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.conditionLock = [[NSConditionLock alloc] init];
[self dataArrTest];
}
- (void)dataArrTest{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
[self.conditionLock lock];
NSLog(@"__one");
sleep(1);
[self.conditionLock unlockWithCondition:2];
});
dispatch_async(queue, ^{
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
sleep(1);
[self.conditionLock unlockWithCondition:3];
});
dispatch_async(queue, ^{
[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
sleep(1);
[self.conditionLock unlock];
});
}
@end
代码输出:
__one __two __three
全局并发队列,异步操作,这里就是使用NSConditionLock达到了线程依赖的功能,使用NSOperation也可以,我们用图说明一下:
3.9 dispatch_semaphore
GCD提供了dispatch_semaphore方案来处理多线程同步问题。
3.10 @synchronized
这个使用很简单,也比较常见:
@synchronized (lockObj) {
/*
加锁代码(临界区)
*/
}
但是它是所有线程同步方案里面性能最差的。
开启汇编调试,发现@synchronized
在执行过程中,会走底层的 objc_sync_enter
和 objc_sync_exit
方法:
也可以通过 clang,查看底层编译代码:
通过对objc_sync_enter
方法进行符号断点,查看底层所在的源码库,通过断点发现在objc
源码中,进入objc_sync_enter
的源码实现:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {//传入不为nil
SyncData* data = id2data(obj, ACQUIRE);//重点
ASSERT(data);
data->mutex.lock();//加锁
} else {//传入nil
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
主要操作是判断 obj 有值,然后通过id2data
获取了相应的 SyncData
,然后进行了加锁操作。
我们再看下objc_sync_exit
源码实现:
// End synchronizing on 'obj'. 结束对“ obj”的同步
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {//obj不为nil
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();//解锁
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {//obj为nil时,什么也不做
// @synchronized(nil) does nothing
}
return result;
}
这个方法在获取了对应的 SyncData
之后,进行了解锁操作。
通过上面两个实现逻辑发现,在 obj 存在时,都会通过id2data
方法,获取SyncData
。
进入 SyncData
,是一个结构体:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
其内部有一个结构体的指针struct SyncData* nextData
,类似于链表
结构,有 next
指向,并且封装了recursive_mutex_t mutex
,确认@synchronized
是一个递归互斥锁
。
进入SyncCache
定义,也是一个结构体,用于存储线程
,其中 list[0]
表示当前线程的链表 data
,主要用于存储 SyncData 和 lockCount
:
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
我们现在来看下主要的id2data
方法,这个是加锁和解锁都会调用的方法:
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS //tls(Thread Local Storage,本地局部的线程缓存)
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
//通过KVC方式对线程进行获取 线程绑定的data
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
//如果线程缓存中有data,执行if流程
if (data) {
fastCacheOccupied = YES;
//如果在线程空间找到了data
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
//通过KVC获取lockCount,lockCount用来记录 被锁了几次,即 该锁可嵌套
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
switch(why) {
case ACQUIRE: {
//objc_sync_enter走这里,传入的是ACQUIRE -- 获取
lockCount++;//通过lockCount判断被锁了几次,即表示 可重入(递归锁如果可重入,会死锁)
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//设置
break;
}
case RELEASE:
//objc_sync_exit走这里,传入的why是RELEASE -- 释放
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);//判断缓存中是否有该线程
//如果cache中有,方式与线程缓存一致
if (cache) {
unsigned int I;
for (i = 0; i < cache->used; i++) {//遍历总表
SyncCacheItem *item = &cache->list[I];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE://加锁
item->lockCount++;
break;
case RELEASE://解锁
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache 从cache中清除使用标记
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
//第一次进来,所有缓存都找不到
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {//cache中已经找到
if ( p->object == object ) {//如果不等于空,且与object相似
result = p;//赋值
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);//对threadCount进行++
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object 没有与当前对象关联的SyncData
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it 第一次进来,没有找到
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));//创建赋值
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) { //判断是否支持栈存缓存,支持则通过KVC形式赋值 存入tls
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);//lockCount = 1
} else
#endif
{
// Save in thread cache 缓存中存一份
if (!cache) cache = fetch_cache(YES);//第一次存储时,对线程进行了绑定
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
第一步:首先在 tls 即线程缓存中查找。
a.在 tsl_get_direct
方法中以线程为key
,通过 kvc
的方式获取与之绑定的 SyncData
,即线程 data
,其中的tls()
,表示本地局部的线程缓存;
b.判断获取的data
是否存在,以及判断data
中是否能找到对应的 object
;
c.如果都找到了,在tsl_get_direct
方法中以 KVC
的方式获取 lockCount
,用来记录对象被锁了几次(即锁的嵌套次数)
d.如果data
中的threadCount
小于等于0,或者lockCount
小于等于0时,则直接崩溃
e.通过传入的why
,判断操作类型:
e.1 如果是ACQUIRE
,表示加锁,则进行lockCount++
,并保存到tls缓存
e.2 如果是RELEASE
,表示释放,则进行lockCount--
,并保存到tls
缓存。如果lockCount
等于 0,从tls
中移除线程data
e.3 如果是CHECK
,则什么也不做
第二步:如果 tls
中没有,则在cache缓存
中查找
a.通过 fetch_cache
方法查找 cache
缓存中是否有线程
b.如果有,则遍历 cache
总表,读取线程对应的 SyncCacheItem
c. 从SyncCacheItem
中取出data
,然后后续步骤与tls
的匹配是一致的
第三步:如果 cache
中也没有,即第一次进来,则创建 SyncData
,并存储到相应缓存中:
a.如果在cache
中找到线程,且与object
相等,则进行赋值、以及threadCount++
b.如果在cache
中没有找到,则threadCount
等于1
针对tls
和 cache
缓存,底层的表结构如下:
- 哈希表结构中通过
SyncList
结构来组装多线程的情况 -
SyncData
通过链表的形式组装当前可重入的情况 - 下层通过
tls
线程缓存、cache
缓存来进行处理 - 底层主要有两个东西:
lockCount
、threadCount
,解决了递归互斥锁,解决了嵌套可重入
3.11 总结
我们对iOS的各种线程同步方案体验了一下:
-
OSSpinLock
由于其不休眠特性,所以它的效率是非常高的,但是由于安全问题,苹果建议我们使用os_unfair_lock
取而代之,并且效率还要高于前者。 -
pthread_mutex
是一种跨平台的解决方案,性能也不错。当然还有苹果的GCD
解决方案,也是挺不错的。 - 对于
NS
开头的那些OC
下的解决方案,虽然本质也还是基于pthread_mutex
的封装,但是由于多了一些面向对象的操作开销,效率不免要下降。 - 性能最差的是
@synchronized
方案,虽然它的使用是最简单的,但因为它的底层封装了过于复杂的数据结构
,导致了性能低下。
使用推荐如下: - os_unfair_lock(推荐🌟🌟🌟🌟🌟)
- OSSpinLock(不安全⚠️⚠️)
- dispatch_semaphore(推荐🌟🌟🌟🌟🌟)
- pthread_mutex(推荐🌟🌟🌟🌟)
- dispatch_queue(DISPATCH_QUEUE_SERIAL)(推荐🌟🌟🌟)
- NSLock(🌟🌟🌟)
- NSCondition(🌟🌟🌟)
- pthread_mutex(recursive)(🌟🌟)
- NSRecursiveLock(🌟🌟)
- NSConditionLock(🌟🌟)
- @synchronized(最不推荐)
4. 自旋锁和互斥锁的对比
4.1 什么情况下选择自旋锁更好?
自旋锁的特点:效率高、安全性不足、占用CPU资源大,因此选择自旋锁依据原则如下:
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但是竞争的情况发生概率很小,对安全性要求不高
-
CPU
资源不紧张 - 多核处理器
4.2 什么情况下选择互斥锁更好?
互斥锁特点:安全性突出、占用CPU资源小,休眠/唤醒过程要消耗CPU资源,因此选择互斥锁依据原则如下:
- 预计线程等待锁的时间比较长
- 单核处理器
- 临界区有
IO操作
- 临界区代码复杂或者循环量大
- 临界区的竞争非常激烈,对安全性要求高
4.3 为什么iOS中几乎不用atomic
?
atomic
是用于保证属性的setter、getter
方法的原子性操作的,本质就是在getter和setter
内部增加线程同步的锁,用的锁实质上也是os_unfair_lock
。
但是atomic
只是保证读写操作的线程同步,对于可变数组、可变字典来说,如果你使用atomic
修饰,在添加元素的时候,并不是线程安全的:
NSMutableArray *arr = self.dataArr;//getter方法是安全的
for (int i = 0; i<5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[arr addObject:@"1"];//这里会有多线程操作_dataArr,atomic无法保证这里的线程同步
});
}
所以说atomic
并不能完全保证多线程安全问题。
我们几乎不会用到atomic
,因为property
在iOS
代码中调用的太频繁了,会导致锁的过度使用,消耗CPU
资源,所以我们只需要针对具体会出现多线程隐患的地方加锁就行了,需要加锁的时候再去加。
4.4 多线程读写安全
在上面存取钱的示例中,存、和取其实就是对共享资源的读和写
,假如我们有如下两个操作分别只包含读操作和写操作:
- (void)read {
sleep(1);
NSLog(@"read");
}
- (void)write
{
sleep(1);
NSLog(@"write");
}
其实读操作的目的,只是取出数据,并不会修改数据,所以多线程同时进行读操作
是没问题的,不需要考虑线程同步的问题,写操作
是导致多线程安全问题的根本因素。所以为了读写安全,解决方案其实就是多读单写:
- 要求1: 同一时间,只能有1个线程进行写的操作
- 要求2: 同一时间,允许有多个线程进行读的操作
- 要求3: 同一时间,不允许既读又写,就是说读操作和写操作之间是互斥关系
iOS有两种方案可以实现上述的读写安全需求
-
pthread_rwlock:
读写锁 -
dispatch_barrier_async:
异步栅栏调用
-
pthread_rwlock
使用pthread_rwlock
,等待锁的线程会进入休眠,API如下:
//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
//读操作加锁
pthread_rwlock_rdlock(&lock);
//读操作尝试加锁
pthread_rwlock_tryrdlock(&lock);
//写操作加锁
pthread_rwlock_wrlock(&lock);
//写操作尝试加锁
pthread_rwlock_trywrlock(&lock);
//解锁
pthread_rwlock_unlock(&lock);
//销毁锁
pthread_rwlock_destroy(&lock);
-
dispatch_barrier_async
使用dispatch_barrier_async
有一个注意点,这个函数接受的并发队列参数必须是你自己手动创建的(dispatch_queue_create)
,如果接受的是一个串行队列或者是一个全局并发队列,那么这个函数的效果等同于dispatch_async
函数。
具体介绍,可以看另外一篇文章,里面有对于GCD
常用方法的介绍:
//www.greatytc.com/p/3f9910293401