mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
docs: SSL/TLS
This commit is contained in:
@@ -325,22 +325,23 @@ dispatch_group_notify(group, queue, ^{
|
||||
|
||||
QA:优先级反转是什么?
|
||||
|
||||
线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。也就是时间片轮转调度算法(进程、线程)。同时不同线程的优先级不一样
|
||||
线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。
|
||||
|
||||
比如存在 thread1 的优先级最高、thread2 优先级普通。CPU 在调度到 thread2 的时候会加,加锁后 CPU 调度到 thread1,在尝试给 thread1 加锁的时候发现锁被占用,所以此时在 thread1 里面自旋等待锁。thread2 等待 CPU 调度过来,但是因为 thread1 优先级比较高,所以 CPU 优先执行 thread1,可是 thread1 里等待锁,锁此时被 thread2 占用。存在死锁
|
||||

|
||||
|
||||
```
|
||||
thread2 {
|
||||
加锁
|
||||
做事情
|
||||
解锁
|
||||
}
|
||||
thread1 {
|
||||
加锁(等待锁) while(未解锁)
|
||||
做事情
|
||||
解锁
|
||||
}
|
||||
```
|
||||
线程 A 在 T1 时刻拿到锁,并处理数据。
|
||||
|
||||
线程 C 在 T2 时刻被唤醒,但是此时锁被线程 A 使用,所以线程 C 放弃 CPU 进入阻塞状态,而线程 A 继续占据 CPU,执行任务
|
||||
|
||||
目前来看一切正常。
|
||||
|
||||
但是在 T3 时刻,线程 B 被唤醒,由于优先级比较高,所以会立即抢占 CPU,此时线程 A 被迫进入 READY 状态等待。
|
||||
|
||||
T4 时刻,线程 B 放弃 CPU,此时线程 A (优先级10)是唯一处于 READY 状态的线程,所以再次占据 CPU 去执行任务,在 T5 时刻释放锁。
|
||||
|
||||
在 T5 时刻,线程 A 解锁瞬间,线程 C 立即获取锁,并在优先级20上等待 CPU,因为优先级比较高,所以系统会立刻调度线程 C 的任务执行。此时线程 A 进入 READY 状态。
|
||||
|
||||
线程 B 从 T3 到 T4 这个时间段占据 CPU 资源的行为叫做优先级反转。一个优先级 15 的线程B,通过压制优线级10的线程 A,而事实上导致高优先级线程 C 无法正确得到 CPU。这段时间是不可控的,因为线程 B 可以长时间占据 CPU(即使轮转时间片到时,线程 A 和 B 都处于可执行态,但是因为B的优先级高,它依然可以占据 CPU),其结果就是高优先级线程 C 可能长时间无法得到 CPU。
|
||||
|
||||
上面的代码改进下
|
||||
|
||||
@@ -436,13 +437,13 @@ int cursorr = 1;
|
||||
|
||||
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
|
||||
|
||||

|
||||

|
||||
|
||||
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
|
||||
|
||||
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
|
||||
|
||||

|
||||

|
||||
|
||||
### pthread_mutex
|
||||
|
||||
@@ -515,8 +516,6 @@ int cursor = 0;
|
||||
|
||||
互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
|
||||
|
||||
|
||||
|
||||
### 互斥条件锁 pthread_cond_t
|
||||
|
||||
初始化互斥锁条件 `pthread_cond_init(&_condition, NULL);`
|
||||
@@ -596,10 +595,6 @@ int cursor = 0;
|
||||
|
||||
- add 方法内加完元素会调用 `pthread_cond_signal` 来激活等待该条件的线程
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 从汇编角度分析 os_unfair_lock 属于什么锁(教你如何用汇编分析源码)
|
||||
|
||||
属于互斥锁。自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。
|
||||
@@ -637,21 +632,21 @@ si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看
|
||||
|
||||
第一步:当第二次调用 saveMoney 方法,开启汇编调试
|
||||
|
||||

|
||||

|
||||
|
||||
看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
|
||||
|
||||
第二步:继续输入 si,敲回车
|
||||
|
||||

|
||||

|
||||
|
||||
第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
|
||||
|
||||

|
||||

|
||||
|
||||
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
|
||||
|
||||

|
||||

|
||||
|
||||
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,太浪费性能了。
|
||||
|
||||
@@ -665,10 +660,6 @@ si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看
|
||||
|
||||
同样的步骤研究 `pthread_mutex_t` 会发现最后也是调用 `syscall` 做到线程休眠,不像自旋锁一样,在底层实现是 while 循环一样忙等,浪费资源。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### NSLock、NSRecursiveLock
|
||||
|
||||
NSLock 是对 mutex 普通锁(pthread_mutex_t)的封装
|
||||
@@ -679,7 +670,7 @@ NSRecursiveLock 是对 mutex 递归锁(pthread_mutex_t ,且 attr 为 `PTHREA
|
||||
|
||||
```objectivec
|
||||
+ (void) initialize{
|
||||
static BOOL beenHere = NO;
|
||||
static BOOL beenHere = NO;
|
||||
if (beenHere == NO){
|
||||
beenHere = YES;
|
||||
/* Initialise attributes for the different types of mutex.
|
||||
@@ -735,7 +726,7 @@ NSRecursiveLock 是对 mutex 递归锁(pthread_mutex_t ,且 attr 为 `PTHREA
|
||||
- (id) init{
|
||||
if (nil != (self = [super init])) {
|
||||
if (0 != pthread_mutex_init(&_mutex, &attr_recursive)){
|
||||
DESTROY(self);
|
||||
DESTROY(self);
|
||||
}
|
||||
}
|
||||
return self;
|
||||
@@ -749,7 +740,7 @@ pthread_mutexattr_init(&attr_recursive);
|
||||
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
|
||||
```
|
||||
|
||||
|
||||
NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多线程下递归调用。底层原因是 TLS 有关。
|
||||
|
||||
### NSCondition
|
||||
|
||||
@@ -814,7 +805,7 @@ Demo
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
存在 `虚假唤醒` 的问题。则可以将后续的 if 判断换为 while。比如某一时刻发送了一次 signal,然后可能有多个线程收到唤醒的信号,则可能还是会存在问题。所以 if 换为 while。
|
||||
|
||||
### NSCondtionLock
|
||||
|
||||
@@ -856,20 +847,12 @@ Demo
|
||||
|
||||
通过 NSCondtionLock 可以控制线程的执行顺序。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### dispatch_queue
|
||||
|
||||
使用 GCD 的串行队列,也是可以实现线程同步。
|
||||
|
||||
线程同步的本质就是多线程的任务是顺序执行
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### dispatch_semaphore
|
||||
|
||||
semaphore 叫做”信号量”
|
||||
@@ -912,16 +895,35 @@ semaphore 叫做”信号量”
|
||||
|
||||
`dispatch_semaphore_signal` 函数的本质:让信号量的值 + 1
|
||||
|
||||
|
||||
|
||||
所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行
|
||||
|
||||
|
||||
NSCache 扩容策略。x86 3/4,arm7/8.
|
||||
|
||||
有趣的实验:
|
||||
|
||||
```objectivec
|
||||
self.semaphore = dispatch_semaphore_create(1);
|
||||
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
|
||||
```
|
||||
|
||||
上面的代码会 crash。因为创建出来信号量为1,但是经过 dispatch_semaphore_wait 之后信号量变为0,底层会调用到 `_dispatch_semaphore_dispose`。内部会做判断,就是原始的信号量
|
||||
|
||||
```objectivec
|
||||
void _dispatch_semaphore_dispose(dispatch_object_t dou,
|
||||
DISPATCH_UNUSED bool *allow_free){
|
||||
dispatch_semaphore_t dsema = dou._dsema;
|
||||
if (dsema->dsema_value < dsema->dsema_orig) {
|
||||
DISPATCH_CLIENT_CRASH(dsema->dsema_orig - dsema->dsema_value,
|
||||
"Semaphore object deallocated while in use");
|
||||
}
|
||||
_dispatch_sema4_dispose(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
|
||||
}
|
||||
```
|
||||
|
||||
### @synchronized
|
||||
|
||||
@synchronized 可递归重入的原理分析/线程缓存空间
|
||||
|
||||
```objectivec
|
||||
@interface ViewController ()
|
||||
@property (nonatomic, assign) NSInteger money;
|
||||
@@ -972,7 +974,7 @@ semaphore 叫做”信号量”
|
||||
|
||||
为了探究下实现,开启汇编调试
|
||||
|
||||

|
||||

|
||||
|
||||
可以查看 objc4 的源码,查找 `objc_sync_enter`
|
||||
|
||||
@@ -1088,10 +1090,6 @@ class recursive_mutex_tt : nocopy_t {
|
||||
|
||||
另外 `recursive_mutex_tt` 在初始化的时候传入 `PTHREAD_RECURSIVE_MUTEX_INITIALIZER`,看起来也支持递归。所以 @synchronized 是一个递归互斥锁的封装。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
封装
|
||||
|
||||
有的时候我们需要在方法内部创建 semaphore ,则可以创建宏
|
||||
@@ -1107,11 +1105,8 @@ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
#define SemaphoreEnd \
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 自旋锁、互斥锁对比
|
||||
|
||||
什么情况使用自旋锁比较划算?
|
||||
@@ -1136,11 +1131,7 @@ dispatch_semaphore_signal(semaphore);
|
||||
|
||||
- 临界区竞争非常激烈
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## atomic
|
||||
### atomic
|
||||
|
||||
`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。
|
||||
|
||||
@@ -1155,13 +1146,13 @@ id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
|
||||
// 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);
|
||||
}
|
||||
@@ -1182,7 +1173,7 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
|
||||
}
|
||||
|
||||
id oldValue;
|
||||
id *slot = (id*) ((char*)self + offset);
|
||||
id *slot = (id*) ((char*)self offset);
|
||||
|
||||
if (copy) {
|
||||
newValue = [newValue copyWithZone:nil];
|
||||
@@ -1213,6 +1204,13 @@ void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL ato
|
||||
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
|
||||
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
|
||||
}
|
||||
|
||||
void lock() {
|
||||
lockdebug_mutex_lock(this);
|
||||
// <rdar://problem/50384154>
|
||||
uint32_t opts = OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION | OS_UNFAIR_LOCK_ADAPTIVE_SPIN;
|
||||
os_unfair_lock_lock_with_options_inline(&mLock, (os_unfair_lock_options_t)opts);
|
||||
}
|
||||
```
|
||||
|
||||
可以看到设置属性的时候会判断是不是 atomic
|
||||
@@ -1223,15 +1221,38 @@ void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL ato
|
||||
|
||||
它并不能保证使用属性的过程是线程安全的。
|
||||
|
||||
|
||||
|
||||
QA:为什么在 iOS 上几乎没有使用?
|
||||
|
||||
因为属性 getter、setter 使用太高频,另外 atomic 内部实现是自旋锁,自旋锁是忙等,所以太耗费性能了。
|
||||
|
||||
#### atomic 并不能保证使用属性的过程是线程安全的?
|
||||
|
||||
```objectivec
|
||||
@property (atomic,copy) NSString *name;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
@synchronized(self){
|
||||
for (int i = 0; i<100; i++) {
|
||||
self.name = @"杭城小刘";
|
||||
NSLog(@"线程1 : %@",self.name);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
@synchronized(self){
|
||||
for (int i = 0; i<100; i++) {
|
||||
self.name = @"魅影";
|
||||
NSLog(@"线程2 : %@",self.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
预期:线程 A 打印出来一定是杭城小刘,线程 B 打印出来是魅影。但事实上可能存在乱序。
|
||||
|
||||
atomic 是原子属性,它内部实现是针对属性的 setter、getter 进行加锁(早期实现是自旋旋,因为存在问题,后续替换为了 os_unfair_lock)。但是事实上在进行多线程编程的时候,我们针对数据的操作并不是修改指针本身(思考 NSString 的 getter、setter),而是操作类似 NSArray、NSDictionary 这样的 case。比如 `@property(atomic, strong)NSMutableArray *hobbies;` 如果在多线程情况下进行处理,一边生产者添加数据,一边消费者消费数据,则会产生内存问题。
|
||||
|
||||
所以多线程并发编程来说,推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。
|
||||
|
||||
## 读写安全
|
||||
|
||||
@@ -1245,8 +1266,6 @@ QA:为什么在 iOS 上几乎没有使用?
|
||||
|
||||
- dispatch_barrier_async:异步栅栏调用
|
||||
|
||||
|
||||
|
||||
### pthread_rwlock
|
||||
|
||||
初始化
|
||||
@@ -1268,8 +1287,6 @@ pthread_rwlock_init(&_lock, NULL)
|
||||
|
||||
销毁 `pthread_rwlock_destroy(&_lock);`
|
||||
|
||||
|
||||
|
||||
```objectivec
|
||||
@interface ViewController ()
|
||||
@property (nonatomic, assign) NSInteger money;
|
||||
@@ -1324,8 +1341,6 @@ pthread_rwlock_init(&_lock, NULL)
|
||||
2022-04-09 22:25:25.045020+0800 DDD[13652:333135] read 200
|
||||
```
|
||||
|
||||
|
||||
|
||||
### dispatch_barrier_async
|
||||
|
||||
```objectivec
|
||||
@@ -1346,8 +1361,6 @@ dispatch_barrier_async(self.queue, ^{
|
||||
- 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
|
||||
- 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
|
||||
|
||||
|
||||
|
||||
上 Demo
|
||||
|
||||
```objectivec
|
||||
@@ -1402,4 +1415,50 @@ dispatch_barrier_async(self.queue, ^{
|
||||
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
|
||||
```
|
||||
```
|
||||
|
||||
## 其他常见的多线程编程模式
|
||||
|
||||
### Promise
|
||||
|
||||
Promise 在多线程解决方案中比较常见,比如在前端中 Promise 就是一个标准解决方案。同样的,iOS 界也有三方开发者写的 PromiseKit。也有对应的 AFNetworking Promise 版本。
|
||||
|
||||
Promise 解决了什么问题?
|
||||
|
||||
- 在需要多个操作的时候,我们可能会设置多个回调参数嵌套,导致代码很长,也就是传说中的“回调地狱”(Callback Hell)
|
||||
|
||||
- 丧失了 return 特性
|
||||
|
||||
Promise 就是一个对象,用来传递异步操作的消息。代表了某个未来才会知道结果的事件(也就是异步操作),并且这个事件提供统一的 API,可以供进一步处理
|
||||
|
||||
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称 Incomplete)、Resolved(已完成,又称 Fulfilled)和 Rejected (已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
|
||||
|
||||
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
|
||||
|
||||
```objectivec
|
||||
APIClient.fetchData(...).then().onFailure();
|
||||
```
|
||||
|
||||
### Pipeline
|
||||
|
||||
将一个任务分解为若干个阶段(Stage),前阶段的输出为下阶段的输入,各个阶段由不同的工
|
||||
|
||||
作者线程负责执行。
|
||||
|
||||
各个任务的各个阶段是并行(Parallel)处理的。
|
||||
|
||||
具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务上看,不同任务的各个阶段的执行是并行的。
|
||||
|
||||
### Master-Slave
|
||||
|
||||
将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既 提高计算效率,又实现了信息隐藏。
|
||||
|
||||
比如 Jekins
|
||||
|
||||
### Serial Thread Confinement
|
||||
|
||||
如果并发任务的执行涉及某个非线程安全对象,而很多时候我们又不希望因此而引入锁。
|
||||
|
||||
通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化任务创建唯一的工作者线程进行处理。
|
||||
|
||||
比如 FMDB 的设计,内部就是一个串行队列。
|
||||
Reference in New Issue
Block a user