05 知识点梳理:多线程

1. GCD实现多读单写

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

  • 读写互斥

  • 写写互斥

  • 读读并发

@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, 关于此处的解释,插入一段来自参考文献的引用:

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

  • 这里我们选择 dispatch_barrier_async, 为什么是栅栏函数而不是异步函数或者同步函数,下面分析:

    • 栅栏函数任务:

      • 之前所有的任务执行完毕,并且在它后面的任务开始之前,期间不会有其他的任务执行,这样比较好的促使 写操作一个接一个写(写写互斥),不会乱!

    • 为什么不是异步函数?

      • 应该很容易分析,毕竟会产生混乱!

    • 为什么不用同步函数?

      • 如果读写都操作了,那么用同步函数,就有可能存在:我写需要等待读操作回来才能执行,显然这里是不合理!

2 多线程和锁

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

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

多线程:

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

多线程的几种方式

  • pthread

    • POSIX Thread,缩写称为pthread,是线程的POSIX标准,是一套通用的多线程API可以Unix/Linux/Windows等平台跨平台使用。iOS中基本不使用。

  • NSThread

    • 苹果封装的面向对象的线程类,可以直接操作线程,比起GCDNSThread效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。

  • 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

Last updated