Files
knowledge-kit/Chapter1 - iOS/1.39.md
2022-04-19 17:15:49 +08:00

1405 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多线程探究
> 平时我们经常使用 GCD、锁、队列、block那这些概念和本质到底是什么线程安全如何实现自旋锁、互斥锁区别是什么本文来一探究竟
## 多线程方案
| 技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
| ----------- | -------------------------------------------------------------- | --- | ------- | ---------- |
| pthread | -一套通用的多线程API<br>适用于Unix\Linux\Windows等系统<br/>跨平台\可移植<br/>使用难度大 | C | 开发者手动管理 | 很少,底层监控会用到 |
| NSThread | 使用更加面向对象<br/>简单易用,可直接操作线程对象 | OC | 开发者手动管理 | 偶尔 |
| GCD | 旨在替代NSThread等线程技术<br/>充分利用设备的多核 | C | 系统自动管理 | 经常 |
| NSOperation | 基于GCD底层是GCD<br/>比GCD多了一些更简单实用的功能<br/>使用更加面向对象 | OC | 系统自动管理 | 经常 |
| | 并发队列 | 自定义串行队列 | 主队列(串行) |
| --------- | ------------ | ------------ | ------------ |
| 同步sync | 不开新线程、串行执行任务 | 不开新线程、串行执行任务 | 不开新线程、串行执行任务 |
| 异步async | 开新线程、并发执行任务 | 开新线程、串行执行任务 | 不开新线程、串行执行任务 |
## 多线程死锁
看几个 Demo 观察下死锁情况
Demo1
```objectivec
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// 死锁
```
分析主队列是一个串行队列任务3等待 `dispatch_sync` 内的任务执行完毕,可 `dispatch_sync` 内的任务等待任务3执行互相等待产生死锁
Demo2
```objectivec
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// 1 3 2
```
Demo3
```objectivec
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2")
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
// 1 5 2 Crash
```
分析任务4等待 `dispatch_sync` 内的任务3`dispatch_sync` 内的任务3等待任务4执行互相等待
Demo4
```objectivec
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
// 1 5 2 3 4
```
分析不会死锁。因为在存在2个任务队列。所以会按照顺序各自从队列上取任务执行。
Demo5
```objectivec
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
// 1 5 2 3 4
```
总结:
- 队列决定了任务执行完是否需要等待。任务决定是否可以产生新线程
- 死锁:使用`sync` 函数给当前串行队列派发任务,则会卡住当前串行队列,产生死锁
Demo6
```objectivec
- (void)test{
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:3];
NSLog(@"3");
});
}
// 1 3
```
分析为什么打印1、3没有打印2。因为 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 底层是开启了定时器,定时器运行需要添加到 RunLoop。上述代码是在全局并发队列上开启子线程子线程中没有 RunLoop所以定时器没有运行。
Demo7
```objectivec
- (void)test{
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil];
NSLog(@"3");
});
}
// 1 3 2
```
分析为什么现在又执行打印2了因为 `-(id)performSelector:(SEL)aSelector withObject:(id)object;` 是 Runtime API本质上就是 `objc_msgSend`,所以不需要 RunLoop 便可运行
查看 objc4 `NSObject.m` 即可
```c
+ (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj);
}
```
## iOS 底层研究宝藏
如何查看 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 源码。
- 未开源,但是可以设置断点查看汇编分析;
- Apple 的 XNU 是参考 [GNUstep](http://www.gnustep.org/resources/downloads.php#core),它将 Cocoa 的 OC 库重新实现并开源。虽然不是官方源代码,但是具有研究参考价值
查看 GUNStep 源码
```c
- (void) performSelector: (SEL)aSelector
withObject: (id)argument
afterDelay: (NSTimeInterval)seconds
{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
GSTimedPerformer *item;
item = [[GSTimedPerformer alloc] initWithSelector: aSelector
target: self
argument: argument
delay: seconds];
[[loop _timedPerformers] addObject: item];
RELEASE(item);
[loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}
```
可以看到底层实现就是 开启一个 Timer 并添加到 RunLoop。但是没有 Run。所以代码改下就可运行。
```objectivec
- (void)test{
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:3];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"3");
});
}
// 1 2 3
```
所以要研究 iOS 底层的同学,看看 **GUNStep 代码吧,这是宝藏**
## 队列组
- 实现异步并发执行任务1、任务2
- 等任务1、2都执行完毕再回到主线程执行任务3
```objectivec
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
for (NSInteger index = 0; index< 5; index++) {
NSLog(@"Task1: %@ - index:%zd", [NSThread currentThread], index);
}
});
dispatch_group_async(group, queue, ^{
for (NSInteger index = 0; index< 5; index++) {
NSLog(@"Task2: %@ - index:%zd", [NSThread currentThread], index);
}
});
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
for (NSInteger index = 0; index< 5; index++) {
NSLog(@"Task3: %@ - index:%zd", [NSThread currentThread], index);
}
});
});
```
## 多线程安全问题
多线程存在资源共享问题。比如1块内存可能会被多个线程共享同时读或者写导致不一致很容易引发数据错乱和数据安全问题。典型的生产者消费者问题
比如多个线程访问同一个对象、同一个变量、同一个文件
解决方案
使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁。
常见的锁有:
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- @synchronized
### OSSpinLock
`OSSpinLock` 叫做”自旋锁”。
使用的时候需要导入 `#import <libkern/OSAtomic.h>`
`OSSpinLock lock = OS_SPINLOCK_INIT` 初始化
`OSSpinLockLock(&lock);` 加锁
`OSSpinLockUnlock(&lock);` 解锁
`bool res = OSSpinLockTry(&lock)` 尝试加锁(如果需要等待就不加锁直接返回 false如果不需等待则加锁返回 true
存在问题:
- 等待锁的线程会处于忙等busy-wait状态一直占用着 CPU 资源
- 不安全,可能会出现优先级反转问题
- 如果等待锁的线程优先级较高它会一直占用着CPU资源优先级低的线程就无法释放锁
```objectivec
@interface ViewController ()
@property (assign, nonatomic) OSSpinLock bankLock;
@property (nonatomic, assign) NSInteger money;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.bankLock = OS_SPINLOCK_INIT;
self.money = 100;
[self moneyTest];
}
- (void)moneyTest {
dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self withdrawMoney];
}
});
// 100 + 10*50 - 10*10 = 500
}
- (void)saveMoney {
OSSpinLockLock(&_bankLock);
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney += 50;
self.money = previousMoney;
NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_bankLock);
}
- (void)withdrawMoney {
OSSpinLockLock(&_bankLock);
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney -= 10;
self.money = previousMoney;
NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_bankLock);
}
@end
```
QA优先级反转是什么
线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。也就是时间片轮转调度算法(进程、线程)。同时不同线程的优先级不一样
比如存在 thread1 的优先级最高、thread2 优先级普通。CPU 在调度到 thread2 的时候会加,加锁后 CPU 调度到 thread1在尝试给 thread1 加锁的时候发现锁被占用,所以此时在 thread1 里面自旋等待锁。thread2 等待 CPU 调度过来,但是因为 thread1 优先级比较高,所以 CPU 优先执行 thread1可是 thread1 里等待锁,锁此时被 thread2 占用。存在死锁
```
thread2 {
加锁
    做事情
    解锁
}
thread1 {
加锁(等待锁) while(未解锁)
    做事情
    解锁
}
```
上面的代码改进下
```objectivec
- (void)saveMoney {
if (OSSpinLockLock(&_bankLock)) {
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney += 50;
self.money = previousMoney;
NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_bankLock);
}
}
- (void)withdrawMoney {
if (OSSpinLockLock(&_bankLock)) {
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney -= 10;
self.money = previousMoney;
NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_bankLock);
}
}
```
### os_unfair_lock
`os_unfair_lock` 用于取代不安全的 `OSSpinLock` 从iOS10开始才支持。使用的时候需要导入头文件 `#import <os/lock.h>`
从底层调用看,等待 `os_unfair_lock` 锁的线程会处于休眠状态,并非忙等(自旋锁会忙等)
初始化 `os_unfair_lock moneylock = OS_UNFAIR_LOCK_INIT;`
加锁 `os_unfair_lock_lock(&_moneylock);`
解锁 `os_unfair_lock_unlock(&_moneylock);`
尝试加锁 `os_unfair_lock_trylock(&_moneylock)`
继续对存取钱 Demo 用 `os_unfair_lock` 实现
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, assign) os_unfair_lock moneylock;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.moneylock = OS_UNFAIR_LOCK_INIT;
self.money = 100;
[self moneyTest];
}
- (void)moneyTest {
dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self withdrawMoney];
}
});
// 100 + 10*50 - 10*10 = 500
}
int cursorr = 1;
- (void)saveMoney {
NSLog(@"current cursor %d", cursorr);
cursorr++;
os_unfair_lock_lock(&_moneylock);
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney += 50;
self.money = previousMoney;
NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
os_unfair_lock_unlock(&_moneylock);
}
- (void)withdrawMoney {
os_unfair_lock_lock(&_moneylock);
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney -= 10;
self.money = previousMoney;
NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
os_unfair_lock_unlock(&_moneylock);
}
@end
```
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Thread-deadlock-unfaillock.png)
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Thread-deadlock-unfairTrylock.png)
### pthread_mutex
`mutex` 叫做”互斥锁”,等待锁的线程会处于休眠状态。使用时需要引入 `#import <pthread.h>`
使用:
```objectivec
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(&_moneyLock, &attr);
// 释放属性内存
pthread_mutexattr_destroy(&attr);
// 加锁
pthread_mutex_lock(&_moneyLock);
// 解锁
pthread_mutex_unlock(&_moneyLock);
// 释放锁内存
pthread_mutex_destroy(&_moneyLock);
```
其中 `pthread_mutexattr_settype(pthread_mutexattr_t *, int);` 第二个参数有4个枚举值
```objectivec
/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
```
如果类型选 `PTHREAD_MUTEX_DEFAULT` 或者 `PTHREAD_MUTEX_NORMAL` 则可以省略 `pthread_mutexattr_t` 的创建,直接传 NULL`pthread_mutex_init(&_moneyLock, NULL)`
使用如下
```objectivec
- (void)saveMoney {
pthread_mutex_lock(&_moneyLock);
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney += 50;
self.money = previousMoney;
NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
pthread_mutex_unlock(&_moneyLock);
}
```
如果在某个方法内部递归调用自身怎么实现,好像挺简单的,直接内部调用即可。
```objectivec
int cursor = 0;
- (void)sayHi {
if (cursor<10) {
pthread_mutex_lock(&_moneyLock);
cursor++;
NSLog(@"Hi %d", cursor);
[self sayHi];
pthread_mutex_unlock(&_moneyLock);
}
}
// Hi 1
```
只打印了 1。为什么因为第一次调用正常加锁然后递归调用自身第二次调用的时候尝试加锁但是这时候第一次调用时候锁还没释放。
互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
### 互斥条件锁 pthread_cond_t
初始化互斥锁条件 `pthread_cond_init(&_condition, NULL);`
等待条件进入休眠,放开 mutex 锁,被唤醒后会再次对 mutex 加锁 `pthread_cond_wait(&_condition, &_moneyLock);`
激活一个等待该条件的线程 `pthread_cond_signal(&_condition)`
激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)`
```objectivec
@interface ViewController ()
@property (nonatomic, assign) pthread_mutex_t moneyLock;
@property (nonatomic, assign) pthread_cond_t condition;
@property (nonatomic, strong) NSMutableArray *array;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(&_moneyLock, &attr);
// 释放属性内存
pthread_mutexattr_destroy(&attr);
// 加锁
pthread_mutex_lock(&_moneyLock);
// 解锁
pthread_mutex_unlock(&_moneyLock);
// 初始化互斥锁条件
pthread_cond_init(&_condition, NULL);
[self test];
self.array = [NSMutableArray array];
}
- (void)test{
[[[NSThread alloc] initWithTarget:self selector:@selector(delete) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start];
}
- (void)add{
NSLog(@"add beign");
pthread_mutex_lock(&_moneyLock);
[self.array addObject:@"bb"];
NSLog(@"加元素 %zd", self.array.count);
pthread_cond_signal(&_condition);
pthread_mutex_unlock(&_moneyLock);
NSLog(@"add beign");
}
- (void)delete{
NSLog(@"delete beign");
pthread_mutex_lock(&_moneyLock);
if (self.array.count == 0) {
pthread_cond_wait(&_condition, &_moneyLock);
}
[self.array removeLastObject];
NSLog(@"减元素 %zd", self.array.count);
pthread_mutex_unlock(&_moneyLock);
NSLog(@"delete end");
}
@end
// delete beign
// add beign
// 加元素 1
// add beign
// 减元素 0
// delete end
```
可以看到同时调用 delete、add 方法
- 执行 delete 方法先加锁,但是由于数组为空,这时候就不需要执行删除元素,然后执行 add 方法
- add 方法要加锁,发现锁被 delete 方法占用了
- delete 方法为了等有元素再去执行 delete 引入了互斥锁条件 `pthread_cond_t`,调用 `pthread_cond_wait` 。此时线程进入休眠,同时会释放锁。
- add 方法内加完元素会调用 `pthread_cond_signal` 来激活等待该条件的线程
### 从汇编角度分析 os_unfair_lock 属于什么锁(教你如何用汇编分析源码)
属于互斥锁。自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。
> Low-level lock that allows waiters to block efficiently on contention.
系统说它是低级锁,等不到锁就休眠。
测试代码
```objectivec
dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(saveMoney) object:nil] start];
}
- (void)saveMoney {
OSSpinLockLock(&_bankLock);
NSInteger previousMoney = self.money;
sleep(600);
previousMoney += 50;
self.money = previousMoney;
NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_bankLock);
}
```
为了调试方便开启10个线程去执行 `saveMoney` 方法为了查看自旋锁的等是什么实现。我们给里面休眠600s。同时 Xcode - Debug - DebugWorkflow - Always Show Disassembly
lldb 模式下调试汇编有几个指令
c 代表 continue
sistep instruction简写为 stepisi。当你在 Xcode 汇编面板看到某个认识或者可疑符号,断点在这一行的时候,在下方 lldb 面板,属于 si即可进入内部实现。
第一步:当第二次调用 saveMoney 方法,开启汇编调试
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble2.png)
看到可疑方法 `OSSpinLockLock`给它加断点看到第10行高亮了。lldb 模式输入 c敲回车。次数输入 si 即可进入 `OSSpinLockLock`  方法内部调试
第二步:继续输入 si敲回车
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble3.png)
第三步:看到可疑方法 `_OSSpinLockLockSlow`给它加断点lldb 输入 C。此时断点到这一行了继续输入 si。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble4.png)
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLockAssemble1.png)
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,太浪费性能了。
同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble1.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble2.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble3.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble4.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble5.png)
可以看到最后一步调用到了 `syscall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了
同样的步骤研究 `pthread_mutex_t` 会发现最后也是调用 `syscall` 做到线程休眠,不像自旋锁一样,在底层实现是 while 循环一样忙等,浪费资源。
### NSLock、NSRecursiveLock
NSLock 是对 mutex 普通锁pthread_mutex_t的封装
NSRecursiveLock 是对 mutex 递归锁pthread_mutex_t ,且 attr 为 `PTHREAD_MUTEX_RECURSIVE`的封装API 跟 NSLock 基本一致
查看 GUN 源码可以看看到底是如何实现的
```objectivec
+ (void) initialize{
static BOOL beenHere = NO;
if (beenHere == NO){
beenHere = YES;
/* Initialise attributes for the different types of mutex.
* We do it once, since attributes can be shared between multiple
* mutexes.
* If we had a pthread_mutexattr_t instance for each mutex, we would
* either have to store it as an ivar of our NSLock (or similar), or
* we would potentially leak instances as we couldn't destroy them
* when destroying the NSLock. I don't know if any implementation
* of pthreads actually allocates memory when you call the
* pthread_mutexattr_init function, but they are allowed to do so
* (and deallocate the memory in pthread_mutexattr_destroy).
*/
pthread_mutexattr_init(&attr_normal);
pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
pthread_mutexattr_init(&attr_reporting);
pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
/* To emulate OSX behavior, we need to be able both to detect deadlocks
* (so we can log them), and also hang the thread when one occurs.
* the simple way to do that is to set up a locked mutex we can
* force a deadlock on.
*/
pthread_mutex_init(&deadlock, &attr_normal);
pthread_mutex_lock(&deadlock);
baseConditionClass = [NSCondition class];
baseConditionLockClass = [NSConditionLock class];
baseLockClass = [NSLock class];
baseRecursiveLockClass = [NSRecursiveLock class];
tracedConditionClass = [GSTracedCondition class];
tracedConditionLockClass = [GSTracedConditionLock class];
tracedLockClass = [GSTracedLock class];
tracedRecursiveLockClass = [GSTracedRecursiveLock class];
untracedConditionClass = [GSUntracedCondition class];
untracedConditionLockClass = [GSUntracedConditionLock class];
untracedLockClass = [GSUntracedLock class];
untracedRecursiveLockClass = [GSUntracedRecursiveLock class];
}
}
```
可以看到 NSLock 底层就是 pthread_mutex_t。
再看看 NSRecursiveLock
```objectivec
@implementation NSRecursiveLock
- (id) init{
if (nil != (self = [super init])) {
if (0 != pthread_mutex_init(&_mutex, &attr_recursive)){
DESTROY(self);
}
}
return self;
}
```
底层就是 pthread_mutex_init。参数 `attr_recursive` 其实就是一个递归锁的属性。
```objectivec
pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
```
### NSCondition
NSCondition 是对 mutex 和 cond 的封装。
```objectivec
- (id) init {
if (nil != (self = [super init])) {
if (0 != pthread_cond_init(&_condition, NULL)){
DESTROY(self);
} else if (0 != pthread_mutex_init(&_mutex, &attr_reporting)) {
pthread_cond_destroy(&_condition);
DESTROY(self);
}
}
return self;
}
```
因为 NSCondtion 已经封装好锁和条件所以直接使即可。pthread_mutex_t 需要搭配 pthread_cond_t 一起使用
Demo
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *array;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.condition = [[NSCondition alloc] init];
[self test];
self.array = [NSMutableArray array];
}
- (void)test{
[[[NSThread alloc] initWithTarget:self selector:@selector(delete) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start];
}
- (void)add{
NSLog(@"add beign");
[self.condition lock];
[self.array addObject:@"bb"];
NSLog(@"加元素 %zd", self.array.count);
[self.condition unlock];
[self.condition signal];
NSLog(@"add beign");
}
- (void)delete{
NSLog(@"delete beign");
[self.condition lock];
if (self.array.count == 0) {
[self.condition wait];
}
[self.array removeLastObject];
NSLog(@"减元素 %zd", self.array.count);
[self.condition unlock];
NSLog(@"delete end");
}
@end
```
### NSCondtionLock
`NSConditionLock` 是对 NSCondition 的进一步封装,可以设置具体的条件值。
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, strong) NSConditionLock *condition;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.condition = [[NSConditionLock alloc] initWithCondition:1];
[self test];
}
- (void)test{
[[[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];
}
- (void)task1{
[self.condition lockWhenCondition:1];
NSLog(@"task1");
[self.condition unlockWithCondition:2];
}
- (void)task2{
[self.condition lockWhenCondition:2];
NSLog(@"task2");
[self.condition unlockWithCondition:3];
}
- (void)task3{
[self.condition lockWhenCondition:3];
NSLog(@"task3");
[self.condition unlock];
}
@end
```
通过 NSCondtionLock 可以控制线程的执行顺序。
### dispatch_queue
使用 GCD 的串行队列,也是可以实现线程同步。
线程同步的本质就是多线程的任务是顺序执行
### dispatch_semaphore
semaphore 叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1代表同时只允许1条线程访问资源保证线程同步
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
// 控制线程最大并发量
self.semaphore = dispatch_semaphore_create(5);
[self test];
}
- (void)test{
for (NSInteger index = 0; index<20; index++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(task) object:nil] start];
}
}
- (void)task{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task %@", [NSThread currentThread]);
sleep(5);
dispatch_semaphore_signal(self.semaphore);
}
@end
```
`dispatch_semaphore_wait` 函数的本质
- 如果信号量的值 > 0则会让信号量的值 -1然后继续向下执行代码
- 如果信号量的值 <= 0则线程休眠等待。等待多久取决于第二个参数。直到信号量的值 > 0此时会让信号量的值 -1然后继续向下执行代码
`dispatch_semaphore_signal` 函数的本质:让信号量的值 + 1
所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行
### @synchronized
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.money = 100;
[self moneyTest];
}
- (void)moneyTest {
dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self withdrawMoney];
}
});
// 100 + 10*50 - 10*10 = 500
}
- (void)saveMoney {
@synchronized (self) {
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney += 50;
self.money = previousMoney;
NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
}
}
- (void)withdrawMoney {
@synchronized (self) {
NSInteger previousMoney = self.money;
sleep(0.2);
previousMoney -= 10;
self.money = previousMoney;
NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
}
}
@end
```
`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装
为了探究下实现,开启汇编调试
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/synchronized-asemble.png)
可以查看 objc4 的源码,查找 `objc_sync_enter`
```c
int objc_sync_enter(id obj){
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @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;
}
typedef struct SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
```
可以看到 `@synchronized` 的本质是一个互斥递归锁 `recursive_mutex_t`.
传递一个参数 obj经过 `id2data` 方法得到一个结构体对象,访问结构体对象的成员变量
`mutex`,然后调用 `lock` 方法。
如何根据 obj 获取对象,继续查看 `id2data` 方法。
```c
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LISTFOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
// ...
}
class recursive_mutex_tt : nocopy_t {
pthread_mutex_t mLock;
public:
recursive_mutex_tt() : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) {
lockdebug_remember_recursive_mutex(this);
}
recursive_mutex_tt(const fork_unsafe_lock_t unsafe)
: mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER)
{ }
void lock()
{
lockdebug_recursive_mutex_lock(this);
int err = pthread_mutex_lock(&mLock);
if (err) _objc_fatal("pthread_mutex_lock failed (%d)", err);
}
void unlock()
{
lockdebug_recursive_mutex_unlock(this);
int err = pthread_mutex_unlock(&mLock);
if (err) _objc_fatal("pthread_mutex_unlock failed (%d)", err);
}
void forceReset()
{
lockdebug_recursive_mutex_unlock(this);
bzero(&mLock, sizeof(mLock));
mLock = pthread_mutex_t PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
}
bool tryUnlock()
{
int err = pthread_mutex_unlock(&mLock);
if (err == 0) {
lockdebug_recursive_mutex_unlock(this);
return true;
} else if (err == EPERM) {
return false;
} else {
_objc_fatal("pthread_mutex_unlock failed (%d)", err);
}
}
void assertLocked() {
lockdebug_recursive_mutex_assert_locked(this);
}
void assertUnlocked() {
lockdebug_recursive_mutex_assert_unlocked(this);
}
};
```
可以看到是一个哈希表 `StripedMap`,哈希表工作原理就是传递一个 key经过哈希算法生成索引然后获取对应的值。
内部维护了一个哈希表,一个对象一个锁。
另外 `recursive_mutex_tt` 在初始化的时候传入 `PTHREAD_RECURSIVE_MUTEX_INITIALIZER`,看起来也支持递归。所以 @synchronized 是一个递归互斥锁的封装。
封装
有的时候我们需要在方法内部创建 semaphore ,则可以创建宏
```objectivec
#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);
```
### 自旋锁、互斥锁对比
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作IO一般占用 CPU 资源较多。互斥锁本身就占用 CPU所以不适合
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
## atomic
`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。
可以参考源码 objc4 的 `objc-accessors.mm`
```c
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
```
可以看到在获取属性值的时候,判断是不是 atomic
- 不是 atomic 则直接 return
- 如果是 atomic则调用自旋锁 `slotlock` 加锁取值解锁return
```c
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
```
可以看到设置属性的时候会判断是不是 atomic
- atomic 类型,则直接赋值
- 非 atomic 类型,则先自旋锁加锁、赋值、解锁
它并不能保证使用属性的过程是线程安全的。
QA为什么在 iOS 上几乎没有使用?
因为属性 getter、setter 使用太高频,另外 atomic 内部实现是自旋锁,自旋锁是忙等,所以太耗费性能了。
## 读写安全
- 同一时间只能有1个线程进行写的操作
- 同一时间,允许有多个线程进行读的操作
- 同一时间,不允许既有写的操作,又有读的操作
“多读单写”问题经常用于文件、数据的读写操作。iOS 主流方案有:
- pthread_rwlock读写锁
- dispatch_barrier_async异步栅栏调用
### pthread_rwlock
初始化
```objectivec
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);`
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, assign) pthread_rwlock_t lock;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
pthread_rwlock_init(&_lock, NULL); // 初始化读写锁
self.money = 100;
[self moneyTest];
}
- (void)moneyTest {
dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self read];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
[self write];
}
});
}
- (void)read {
pthread_rwlock_rdlock(&_lock);
NSLog(@"read %zd", self.money);
pthread_rwlock_unlock(&_lock);
}
- (void)write {
pthread_rwlock_wrlock(&_lock);
self.money += 25;
NSLog(@"write %zd", self.money);
pthread_rwlock_unlock(&_lock);
}
@end
2022-04-09 22:25:25.042853+0800 DDD[13652:333135] read 100
2022-04-09 22:25:25.043058+0800 DDD[13652:333126] write 125
2022-04-09 22:25:25.043269+0800 DDD[13652:333135] read 125
2022-04-09 22:25:25.043445+0800 DDD[13652:333126] write 150
2022-04-09 22:25:25.043586+0800 DDD[13652:333135] read 150
2022-04-09 22:25:25.043724+0800 DDD[13652:333126] write 175
2022-04-09 22:25:25.043895+0800 DDD[13652:333135] read 175
2022-04-09 22:25:25.044037+0800 DDD[13652:333126] write 200
2022-04-09 22:25:25.044226+0800 DDD[13652:333135] read 200
2022-04-09 22:25:25.044393+0800 DDD[13652:333135] read 200
2022-04-09 22:25:25.044557+0800 DDD[13652:333135] read 200
2022-04-09 22:25:25.044713+0800 DDD[13652:333135] read 200
2022-04-09 22:25:25.044869+0800 DDD[13652:333135] read 200
2022-04-09 22:25:25.045020+0800 DDD[13652:333135] read 200
```
### dispatch_barrier_async
```objectivec
// 初始化队列
self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT);
// 读
dispatch_async(self.queue, ^{
});
// 写
dispatch_barrier_async(self.queue, ^{
});
```
注意:
- 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
- 如果传入的是一个串行或是一个全局的并发队列那这个函数便等同于dispatch_async函数的效果
上 Demo
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, strong) dispatch_queue_t queue;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT);
self.money = 100;
[self moneyTest];
}
- (void)moneyTest {
dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self read];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 4; i++) {
[self write];
}
});
}
- (void)read {
dispatch_async(self.queue, ^{
NSLog(@"read %zd", self.money);
});
}
- (void)write {
dispatch_barrier_async(self.queue, ^{
self.money += 25;
NSLog(@"write %zd", self.money);
});
}
@end
2022-04-09 22:37:26.476134+0800 DDD[14019:343934] read 100
2022-04-09 22:37:26.476134+0800 DDD[14019:343932] read 100
2022-04-09 22:37:26.476317+0800 DDD[14019:343932] write 125
2022-04-09 22:37:26.476489+0800 DDD[14019:343932] read 125
2022-04-09 22:37:26.476629+0800 DDD[14019:343932] write 150
2022-04-09 22:37:26.476766+0800 DDD[14019:343932] read 150
2022-04-09 22:37:26.476905+0800 DDD[14019:343932] write 175
2022-04-09 22:37:26.477033+0800 DDD[14019:343932] write 200
2022-04-09 22:37:26.477199+0800 DDD[14019:343932] read 200
2022-04-09 22:37:26.477216+0800 DDD[14019:343934] read 200
2022-04-09 22:37:26.477233+0800 DDD[14019:343936] read 200
2022-04-09 22:37:26.477247+0800 DDD[14019:343937] read 200
2022-04-09 22:37:26.477267+0800 DDD[14019:343938] read 200
2022-04-09 22:37:26.477269+0800 DDD[14019:343933] read 200
```