多线程相关

491 阅读15分钟

多线程方案

image.png

GCD

GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

队列和任务

GCD中有两个核心概念,队列和任务。

队列

队列其实就是线程池,在OC中以dispatch_queue_t表示,队列分串行队列和并发队列。

任务

任务其实就是线程执行的代码,在OC中以Block表示。 在队列中执行任务有两种方式:同步执行和异步执行。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备创建新线程的能力。

  • 同步执行(sync): 1、同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。 2、只能在当前线程中执行任务,不会创建新线程。
  • 异步执行(async): 1、异步添加任务到指定的队列中,添加完成可以继续执行后面的代码。 2、可以在新的线程中执行任务,可能会创建新线程。

主队列和全局队列

主队列

主队列是串行队列,只有一个线程,那就是主线程,添加到主队列中的任务会在主线执行。通过dispatch_get_main_queue获取主队列。

全局队列

全局队列是并发队列。可以通过dispatch_get_global_queue获取不同级别的全局队列

同步任务和异步任务

用dispatch_sync来创建同步任务 用dispatch_async来创建异步任务 『主线程』中,『不同队列』+**『不同任务』**简单组合的区别:

img

『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:

img

同步串行

img

img

  • 死锁是因为队列引起的循环等待,而非线程。
    • 首先在主线程执行主队列中的viewDidLoad函数。
    • 当执行到block时,因为是同步,所以需要hold住主线程主队列正在执行的viewDidLoad函数,等执行完主队列block内部代码后,再执行主线程主队列viewDidLoad函数。
    • 所以出现了viewDidLoad等待block的情况。
    • block内的代码要执行,必须等待队列中其他函数执行完,即先进先出
    • 所以出现了block等待viewDidLoad的情况。
    • 最终两个函数相互等待,出现造成死锁

img

  • 上面这种用法没问题
    • 首先在主线程执行主队列中的viewDidLoad函数。
    • 当执行到block时,因为是同步,所以需要hold住主线程主队列正在执行的viewDidLoad函数,等执行完主队列block内部代码后,再执行主线程主队列viewDidLoad函数。
    • 所以出现了viewDidLoad等待block的情况。
    • block内部的代码会在serialQueue的队列中取出,因为serialQueueblock排在最前,所以block会被立即取出,并在主线程中执行。
    • block执行完毕,会执行viewDidLoad剩余代码。

img

  • 因为是并发队列,所以运行队列中的任务一起执行,不需要等待上一个任务执行完再执行下一个,所以不会死锁
  • 如果global_queue换成串行队列,就会产生死锁

异步串行

img

  • 先执行完viewDidLoad,再执行block内的代码。

img

  • 因为子线程默认没有开启runloopperformSelector无法执行。

GCD线程池

img

有几个root队列?

12个。

  • userInteractive、default、unspecified、userInitiated、utility 6个,他们的overcommit版本6个。 支持overcommit的队列在创建队列时无论系统是否有足够的资源都会重新开一个线程。 串行队列和主队列是overcommit的,创建队列会创建1个新的线程。并行队列是非overcommit的,不一定会新建线程,会从线程池中的64个线程中获取并使用。
  • 优先级 userInteractive>default>unspecified>userInitiated>utility>background
  • 全局队列是root队列。

有几个线程池?

两个。一个是主线程池,另一个是除了主线程池之外的线程池。

一个队列最多支持几个线程同时工作?

64个

多个队列,允许最多几个线程同时工作?

64个。优先级高的队列获得的可活跃线程数多于优先级低的,但也有例外,低优先级的也能获得少量活跃线程。 参考资料:[iOS刨根问底-深入理解GCD](

GCD常用

dispatch_sync

dispatch_sync 函数不管是把 block 任务提交到自定义串行队列还是并行队列(主队列比较特殊,这里除外),它都不会开启一条新线程。

原理:将任务 block 通过 push 到队列中,然后按照 FIFO 去执行。dispatch_sync造成死锁的主要原因是调用_dq_state_drain_locked_by判断队列和线程的等待状态,如果堵塞的tid和现在运行的tid为同一个,从而造成死锁的崩溃。

  • 将任务压入队列: _dispatch_thread_frame_push(&dtf, dq);
  • 执行任务 block: _dispatch_client_callout(ctxt, func);
  • 将任务出对列:_dispatch_thread_frame_pop(&dtf);

dispatch_async

  • dispatch_async 函数提交到主队列( dispatch_get_main_queue() )中的多个 block 任务只在主线程中串行执行,不会开启新线程。
  • dispatch_async 函数提交到同一自定义串行队列中的多个 block 任务会开启一条新线程,然后所有的 block 任务 在这一条新线程中串行执行。
  • dispatch_async 函数提交到不同自定义串行队列中的 block 任务则会各自开启一条新线程并行执行。
  • dispatch_async 函数提交到并行队列中的多个 block 任务则会各自开启一条线程并行执行。
  • dispatch_sync 的话不管是提交到串行队列还是并行队列中的 block 任务,它们都是在当前线程中串行执行的,它不会开启新线程。但是这里有一个特殊点,就是如果队列是主队列的话,则 block任务只能在主线程中执行,不管我们通过 dispatch_sync 或者 dispatch_async 方式提交,都是如此。即主队列中和主线程是绑定的,只要我们往主队列中提交任务,那么它必会在主线程中执行。

当然主线程中也可以执行其他任意的我们自定义的队列中的任务  dispatch_async 函数的实现代码在逻辑上可以分为两个部分,第一部分对 work 函数封装(_dispatch_continuation_init)(内部会把 block 复制到堆区 _dispatch_Block_copy(work),即我们使用 dispatch_async 函数时创建的栈区 block 会被直接复制到堆区去),第二部分是对线程的调用(_dispatch_continuation_async)并发处理提交的任务函数。

原理:dispatch_async 会把任务包装并保存,之后就会开辟相应线程去执行已保存的任务。

dispatch_once

可以用disaptch_once来执行一次性的初始化代码,比如创建单例,这个方法是线程安全的。

+ (instancetype)sharedInstance {
    static XXObject *_instance;
    static dispatch_once_t _predicate;
    dispatch_once(&_predicate, ^{
        _instance = [[XXObject alloc] init];
    });
    return _instance;
}

原理:为了标识下面dispatch_once的block是否已执行过,static修饰会默认将_predicate其初始化为0,当值为0时才会执行block。当block执行完成,底层会将onceToken设置为1,这也就是为什么要传onceToken的地址(static修饰的变量可以通过地址修改onceToken的值),同时底层会加锁来保证这个方法是线程安全的。只要当onceToken == 0时才会执行block,否则直接返回静态变量instance。

dispatch_group

使用dispatch_group实现A、B、C三个任务并发,完成后执行任务D。

可以用dispatch_group来实现类似需求,当一组任务都执行完成后,然后再来执行最后的操作。比如进入一个页面同时发起两个网络请求,等两个网络请求都返回后再执行界面刷新。可以用dispatch_group + dispatch_group_enter + dispatch_group_leave + dispatch_group_notify来实现。

img img

原理: dispatch_group 在内部也会维护一个值,当调用 dispatch_group_enter 函数进行进组操作时(dg_bits - 0x0000000000000004ULL),当调用 dispatch_group_leave 函数进行出组操作时(dg_state + 0x0000000000000004ULL)时对该值进行操作(这里可以把 dg_bitsdg_state 理解为一个值),当该值达到临界值 0 时会做一些后续操作(_dispatch_group_wake 唤醒异步执行 dispatch_group_notify 函数添加的所有回调通知),且在使用过程中一定要谨记进组(enter)和出组(leave)必须保持平衡。 GCD 任务 block 与 dispatch_group 关联的方式:

  • 调用 dispatch_group_enter 表示一个 block 与 dispatch_group 关联,同时 block 执行完后要调用 dispatch_group_leave 表示解除关联,否则 dispatch_group_s 会永远等下去。
  • 调用 dispatch_group_async 函数与 block 关联,其实它是在内部封装了一对 enter 和 leave 操作。

dispatch_after

来延迟执行代码。类似NSTimer。需要注意的是:dispatch_after 方法并不是在指定时间之后才开始执行任务,而是在指定时间之后将任务追加到主队列中。

dispatch_semaphore_t

  • 用来计数

    当创建信号量时初始化大于1,可以用来实现多线程并发。

  • 用做锁,效率比较高

    当创建信号量时初始化等于1,退化为锁。信号量锁的效率很高,仅次于OSSpinLock和os_unfair_lock。

原理:GCD 的信号量实际上是基于 mach 内核的信号量接口来实现。 dispatch_semaphore_wait 等待(减少)信号量。减少计数信号量,如果结果值小于零,此函数将等待信号出现,然后返回。semaphore_timedwait 函数即可以指定超时时间。如果小于 0,则调用 _dispatch_semaphore_wait_slow 函数进行阻塞等待

dispatch_semaphore_signal 发信号(增加)信号量。如果先前的值小于零,则此函数在返回之前唤醒等待的线程。如果线程被唤醒,此函数将返回非零值。否则,返回零。_dispatch_semaphore_signal_slow 内部调用 _dispatch_sema4_signal(&dsema->dsema_sema, 1) 唤醒一条线程。

dispatch_barrier_async

利用dispatch_barrier_async实现多读单写

img img

  • 读的时候使用dispatch_sync,是因为使用同步队列可以在赋值结束后,再执行返回值的操作。

原理:dispatch_barrier_async 函数内部和 dispatch_async 相比在 dc_flags 赋值时添加了 DC_FLAG_BARRIER 标记,而此标记正是告知 dispatch_continuation_s 结构体中封装的 block 是一个 barrier block,其它的内容则和 dispatch_async 如出一辙。

 一个 dispatch barrier 允许你在一个并行队列中创建一个同步点。当在队列中遇到这个 barrier block 时,这个 barrier block 便会延迟执行(同时所有在其后的 block 都会延迟),直至所有在 barrier 之前的 block 执行完成。这时,这个 barrier block 便会执行,之后队列便恢复正常执行。

调用这个函数总是会在这个 block 被提交后立刻返回,并且不会等到 block 被触发。当这个 barrier block 到达私有并行 队列最前端时,它不是立即执行。恰恰相反,这个队列会一直等待当前正在执行的队列执行完成。此时 barrier block 才会执行。所有 barrier block 之后提交的 block 会等到 barrier block 执行结束后才会执行。

 这里你指定的并行队列应该是自己通过 dispatch_queue_cretate 创建的。如果你传的是一个串行或是一个全局的并行队列,那这个函数便等同于 dispatch_async 函数效果了。

NSOperation

NSOperation 是一个抽象类,它封装了线程的实现细节,不需要自己管理线程的生命周期和线程的同步等,需要和 NSOperationQueue 一起使用。使用 NSOperation ,你可以方便地控制线程,比如取消线程、暂停线程、设置线程的优先级、设置线程的依赖。NSOperation常用于下载库的实现,比如SDWebImage的实现就用到了NSOperation。

优势、特点

  • 添加任务依赖
  • 任务执行状态控制
  • 最大并发量

任务状态

  • isReady
  • isExecuting
  • isFinished
  • isCancelled

任务状态控制

  • 如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。
  • 如果重写了start方法,自行控制任务状态。

Q:系统是怎样移除一个isFinished=YESNSOperation的?

  • KVO

NSThread

NSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自己来控制的。它的缺点是需要我们自己维护线程的生命周期、线程之间同步等,优点是轻量,灵活。

NSThread启动流程

img

多线程的安全

多线程常见三大问题

竞态条件

两个或两个以上线程对共享数据进行读写操作时,最终数据结果不确定。 image.png image.png 检测方案

打开Xcode中线程检测工具Thread Sanitizer,检测出代码出现竞态条件的地方,提醒我们修改。

处理方案

1.串行队列

无论是读操作还是写操作,同时只能进行一个操作。缺点再有大量读写操作时,只能进行单个该操作,效率太低。

2.串行队列配合异步操作

异步操作由于会直接返回结果,所以必须配合闭包来保证后续操作的合法性。

3.并行队列配合barrier_asycn

在读操作时候使用sync返回操作结果,在写操作的时候使用barrier_asycn来保证并行队列只能进行当前的操作。

优先级倒置

低优先级任务会因为各种原因优先于高优先级的任务执行。

死锁问题

两个或两个以上的线程,他们之间互相等待彼此停止执行,以获得某种资源,但是没有一方会提前退出的情况。

多线程同步

多线程情况下访问共享资源需要进行线程同步,线程同步一般都用锁实现。从操作系统层面,锁的实现有临界区、事件、互斥量、信号量等。这里讲一下iOS中多线程同步的方式。

atomic

使用atomic 修饰属性,编译器会设置默认读写方法为原子读写,底层采用自旋锁(iOS10开始自旋锁改为互斥锁实现了)保证原子操作。 单独的原子操作绝对是线程安全的,但是组合一起的操作就不能保证。一般我们在定义属性的时候用nonatomic,避免性能损失。 参考资料:atomic实现原理

@synchronized

@synchronized指令是一个对象锁,用起来非常简单。使用obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程1和线程2中的@synchronized后面的obj不相同,则不会互斥。@synchronized其实是对pthread_mutex递归锁的封装。 @synchronized优点是我们不需要在代码中显式的创建锁对象,使用简单; 缺点是@synchronized会隐式的添加一个异常处理程序,该异常处理程序会在异常抛出的时候自动的释放互斥锁,从而带来额外开销。

image.png

NSLock

最简单的锁,调用lock获取锁,unlock释放锁。如果其它线程已经调用lock获取了锁,当前线程调用lock方法会阻塞当前线程,直到其它线程调用unlock释放锁为止。NSLock使用简单,在项目中用的最多。

image.png

NSRecursiveLock

递归锁主要用来解决同一个线程频繁获取同一个锁而不造成死锁的问题。注意lock和unlock调用必须配对。

NSConditionLock

条件锁,可以设置自定义条件来获取锁。比如生产者消费者模型可以用条件锁来实现。

NSCondition

条件,操作系统中信号量的实现,方法- (void)wait和- (BOOL)waitUntilDate:(NSDate *)limit用来等待锁直至锁有信号;方法- (void)signal和- (void)broadcast使condition有信号,通知等待condition的线程,变成非阻塞状态。

image.png

dispatch_semaphore_t

信号量的实现,可以实现控制GCD队列任务的最大并发量,类似于NSOperationQueue的maxConcurrentOperationCount属性。

image.png

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。使用pthread_mutex_init创建锁,使用pthread_mutex_lock和pthread_mutex_unlock加锁和解锁。注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁

image.png

OSSpinLock

OSSpinLock 是自旋锁,等待锁的线程会处于忙等状态。一直占用着 CPU。自旋锁就好比写了个 while,whil(被加锁了) ; 不断的忙等,重复这样。OSSpinLock是不安全的锁(会造成优先级反转)

image.png

什么是优先级反转

  • 假设通过OSSpinLock给两个线程thread1thread2加锁
  • thread优先级高, thread2优先级低
  • 如果thread2先加锁, 但是还没有解锁, 此时CPU切换到thread1
  • 因为thread1的优先级高, 所以CPU会更多的给thread1分配资源, 这样每次thread1中遇到OSSpinLock都处于使用状态
  • 此时thread1就会不停的检测OSSpinLock是否解锁, 就会长时间的占用CPU
  • 这样就会出现类似于死锁的问题

os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等

image.png

锁的性能

性能从高到低排序 1、os_unfair_lock 2、OSSpinLock 3、dispatch_semaphore 4、pthread_mutex 5、NSLock 6、NSCondition 7、pthread_mutex(recursive) 8、NSRecursiveLock 9、NSConditionLock 10、@synchronized

自旋锁、互斥锁比较

  • 什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
  • 什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈