> For the complete documentation index, see [llms.txt](https://ryukiedev.gitbook.io/wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ryukiedev.gitbook.io/wiki/zhi-shi-dian-shu-li/zhi-shi-dian-shu-li-san-duo-xian-cheng.md).

# 05 知识点梳理：多线程

## 1. GCD实现多读单写

比如在内存中维护一份数据，有多处地方可能会同时操作这块数据，怎么能保证数据安全？ 这道题目总结得到要满足以下三点：

* 读写互斥
* 写写互斥
* 读读并发

```Swift
@implementation KCPerson
- (instancetype)init
{
    if (self = [super init]) {
       _concurrentQueue = dispatch_queue_create("com.kc_person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
       _dic = [NSMutableDictionary dictionary];
    }
    return self;
}
- (void)kc_setSafeObject:(id)object forKey:(NSString *)key{
    key = [key copy];
    dispatch_barrier_async(_concurrentQueue, ^{
       [_dic setObject:object key:key];
    });
}

- (id)kc_safeObjectForKey：:(NSString *)key{
    __block NSString *temp;
    dispatch_sync(_concurrentQueue, ^{
        temp =[_dic objectForKey：key];
    });
    return temp;
}
@end
```

* 首先我们要维系一个GCD 队列，最好不用全局队列，毕竟大家都知道全局队列遇到栅栏函数是有坑点的，这里就不分析了！
* 因为考虑性能 死锁 堵塞的因素不考虑串行队列，用的是自定义的并发队列！`_concurrentQueue = dispatch_queue_create("com.kc_person.syncQueue", DISPATCH_QUEUE_CONCURRENT);`
* 首先我们来看看读操作:`kc_safeObjectForKey` 我们考虑到多线程影响是不能用异步函数的！说明：
  * 线程2 获取：`name` 线程3 获取 `age`
  * 如果因为异步并发，导致混乱 本来读的是`name` 结果读到了`age`
  * 我们允许多个任务同时进去! 但是读操作需要同步返回，所以我们选择:`同步函数` **（读读并发）**
* 我们再来看看写操作，在写操作的时候对key进行了copy, 关于此处的解释，插入一段来自参考文献的引用:

> 函数调用者可以自由传递一个`NSMutableString`的`key`，并且能够在函数返回后修改它。因此我们必须对传入的字符串使用`copy`操作以确保函数能够正确地工作。如果传入的字符串不是可变的（也就是正常的`NSString`类型），调用`copy`基本上是个空操作。

* 这里我们选择 `dispatch_barrier_async`, 为什么是栅栏函数而不是异步函数或者同步函数，下面分析：
  * 栅栏函数任务：
    * 之前所有的任务执行完毕，并且在它后面的任务开始之前，期间不会有其他的任务执行，这样比较好的促使 写操作一个接一个写`（写写互斥）`，不会乱！
  * 为什么不是异步函数？
    * 应该很容易分析，毕竟会产生混乱！
  * 为什么不用同步函数？
    * 如果读写都操作了，那么用同步函数，就有可能存在：我写需要等待读操作回来才能执行，显然这里是不合理！

## 2 多线程和锁

\*\*进程：\*\*是资源分配的基本单位，它是程序执行时的一个实例，在程序运行时创建。

\*\*线程：\*\*是程序执行的最小单位，是进程的一个执行流，一个进程由多个线程组成。

**多线程：**

一个进程中并发执行多个线程，叫做**多线程**。在一个时间片内，CPU只能**处理一个线程中的一个任务**，对于一个单核CPU来说，在不同的时间片来执行不同线程中的任务，就形成了多个任务在同时执行的“假象”。

### 多线程的几种方式

* `pthread`：
  * 即`POSIX Thread`，缩写称为`pthread`，是线程的POSIX标准，**是一套通用的多线程API**，**可以**在`Unix/Linux/Windows`等平台**跨平台使用**。iOS中基本不使用。
* `NSThread`：
  * **苹果封装的面向对象的线程类，可以直接操作线程**，比起`GCD`，`NSThread`效率更高，**由程序员自行创建**，当线程中的任务执行完毕后，线程会自动退出，**程序员也可手动管理线程的生命周期**。使用频率较低。
* `GCD`：
  * 全称`Grand Central Dispatch`，**由C语言实现，是苹果为多核并行运算提出的解决方案**，CGD会自动利用更多的CPU内核，**自动管理线程的生命周期**，程序员只需要告诉GCD需要执行的任务，**无需编写任何管理线程的代码**。GCD也是iOS使用频率最高的多线程技术。
* `NSOperation`：
  * **基于GCD封装的面向对象的多线程技术，常配合NSOperationQueue使用**，使用频率较高。

### GCD 和 NSOperation 区别

* `GCD`仅仅支持`先进先出FIFO队列`，不支持异步操作之间的依赖关系设置。而`NSOperation`中的队列可以被重新设置优先级，从而实现不同操作的执行顺序调整。
* `NSOperation`支持`KVO`，可以观察任务的执行状态。
* `GCD`更接近底层，`GCD`在追求性能的底层操作来说，是速度最快的。
* 从异步操作之间的事务性，顺序行，依赖关系。`GCD`需要自己写更多的代码来实现，而`NSOperation`已经内建了这些支持。
* 如果异步操作的过程需要更多的被交互和UI呈现出来，`NSOperation`更好。底层代码中，任务之间不太互相依赖，而需要更高的并发能力，`GCD`则更有优势。

### 线程池原理

* 使用线程执行任务的时候，需要到线程池中去取线程进行任务分配。
* 首先判断线程池大小是否小于核心线程池大小，如果小于的话，创建新的线程执行任务；
* 如果当前线程池大小**大于**了核心线程池大小，然后开始判断工作队列是否已满，如果没满，将任务提交到工作队列。
* 如果工作队列已满，判断线程池的线程是否都在工作，如果有空闲线程没有在工作，就交给它去执行任务。
* 如果线程池中的线程都在工作，那么就交给饱和策略去执行。

### 饱和策略分为下面四种：

* `AbortPolicy` 直接抛出`RejectedExecutionExeception` 异常来阻止系统正常运行；
* `CallerRunsPolicy` 将任务回退到调用者；
* `DisOldestPolicy` 丢掉等待最久的任务‘；
* `DisCardPolicy` 直接丢弃任务。

### 线程间通讯

直接同步或者异步向任务队列添加任务。

通过NSPort端口的形式进行发送消息，实现不同的线程间的通信。使用的时候注意需要将NSPort加入的线程的RunLoop中去。

* \*\*直接消息传递：\*\*通过`performSelector`的一系列方法，可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程，这种方式发送的消息将会自动的被序列化。
* \*\*全局变量、共享内存块和对象：\*\*在两个线程之间传递信息的另一种简单方法是使用全局变量，共享对象或共享内存块。尽管共享变量既快速又简单，但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量，以确保代码的正确性。 否则可能会导致竞争状况，数据损坏或崩溃。
* \*\*条件执行：\*\*条件是一种同步工具，可用于控制线程何时执行代码的特定部分。您可以将条件视为关守，让线程仅在满足指定条件时运行。
* **Runloop sources：一个自定义的`Runloop source配置`可以让一个线程上收到特定的应用程序消息。由于`Runloop source`是事件驱动**的，因此在无事可做时，线程会自动进入睡眠状态，从而提高了线程的效率。
* \*\*Ports and sockets：\*\*基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法，但它也是一种非常可靠的技术。更重要的是，端口和套接字可用于与外部实体（例如其他进程和服务）进行通信。为了提高效率，使用`Runloop source`来实现端口，因此当端口上没有数据等待时，线程将进入睡眠状态。
* \*\*消息队列：\*\*传统的多处理服务定义了`先进先出（FIFO）队列`抽象，用于管理传入和传出数据。尽管消息队列既简单又方便，但是它们不如其他一些通信技术高效。
* \*\*Cocoa 分布式对象：\*\*分布式对象是一种 Cocoa 技术，可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信，但是强烈建议不要这样做，因为它会产生大量开销。分布式对象更适合与其他进程进行通信，尽管在这些进程之间进行事务的开销也很高。

### 锁

**线程安全**就是在同一时刻，对同一个数据操作的线程只有一个。这时就用到了锁。

* \*\*锁：\*\*是保证线程安全的同步工具。每一个线程在访问数据资源之前，要先获取(acquire)锁，然后在访问结束之后释放(release)锁。如果锁已经被占用，其它要获取锁的线程会等待，直到锁重新可用。
* iOS中的锁分为**互斥锁**、**自旋锁**、**信号量**这三种。

#### 互斥锁

就是在多线程编程中，防止两条线程同时对同一公共资源(比如全局变量)进行读写。

`@synchronized`：是递归锁

* 调用`synchronzied`的每个对象，`runtime`都会为其分配一个递归锁并存储在哈希表中。
* 如果在`synchronzied`内部对象被释放或为nil，会执行类似`objc_sync_nil`的空方法。
* 注意不要像`synchronzied`传入`nil` , 这将会从代码中移走线程安全。

`NSLock：`遵循 NSLocking 协议，`lock`方法是加锁，`unlock`是解锁，`tryLock`是尝试加锁，如果失败的话返回 NO，`lockBeforeDate`: 是在指定Date之前尝试加锁，如果在指定时间之前都不能加锁，则返回NO。注意不能多次调用lock方法，会造成死锁。

#### 自旋锁

线程会反复检查锁变量是否可用，线程在这一过程中保持执行，是一种忙等待，一旦获取了自旋锁，线程会一直保持该锁，直至显式释放自旋锁，自旋锁避免了进程上下文的调度开销，因此对于线程只会阻塞很短时间的场合是有效的。`atomic`就是通过个set和get方法添加一个自旋锁。

#### 信号量

`dispatch_semaphore`

**使用：**

* 1\). 通过`dispatch_semaphore_create(value)`创建一个信号量，初始为1。
* 2\). 等待信号量`dispatch_semaphore_wait`，可以理解为lock加锁，会使信号量-1。
* 3\). 发送信号量`dispatch_semaphore_signal`，可以理解为 unlock解锁，会使 `signal` 信号量+1。

互斥锁是 `dispatch_semaphore`在取值0/1时的特例。信号量可以有更多的取值空间，用来实现更加复杂的同步，而不仅仅是线程间互斥。

## 3 通知，能不能跨线程

不能跨线程，在哪个线程发送的通知，就会在哪个线程接收。所以需要手动切换到主线程更新UI。

* `测试过程`发出通知的线程决定接收通知处理代码线程
  * `主线程`发通知 — `子线程`监听通知：接收通知代码在主线程处理
  * `子线程`发通知 — `主线程`监听通知：接收通知代码在子线程处理

## 4
