OC 多线程小结

2022/07/28 posted in  Apple
Tags:  #Objective-C

进程与线程

概念

进程

  • 程序运行的一个实例,是资源分配的最小单位,独立地运行在其专用受保护的内存空间内

线程

  • 一个进程可以包含多个线程,线程是操作系统实施调度的最小单位
  • 每个线程之间共享进程的内存空间(代码段、数据段、堆等)及一些进程级的资源(如打开文件等)
  • 每个线程有寄存器、栈、线程局部存储(TLS)等私有数据

串行、并行、并发

串行:任务有序地一个一个地执行,前一个任务执行完之后才能执行下一个任务
并行:多个任务在同一时刻被执行
并发:多个任务在同一时间段内需要被执行,侧重点是这个现象的“发生”

线程的生命周期和调度

16587935108655

RunLoop 和主线程

RunLoop:是事件接收和分发机制的一个实现,可以让线程在适当的时间处理任务不会退出。iOS App 中,主线程的 RunLoop 在程序运行时就会启动,其他线程的 RunLoop 需要手动开启。RunLoop 的基本作用有:保持程序持续运行、处理程序中的各种事件、节省 CPU 资源提高程序性能,在必要的时候线程会被唤醒进行工作,否则会进入休眠,休眠时不占用 CPU。

主线程:iOS App 启动时默认会开启一条线程,称为“主线程”。主线程默认开启 RunLoop,使得主线程可以及时刷新 UI 界面和处理 UI 交互事件(如点击、滑动、拖拽等),所以主线程又称为“UI 线程”。耗时操作会妨碍主线程中的主循环的执行,从而引起 App 卡顿问题。通过多线程编程,将耗时操作放到子线程执行,在必要时再回到主线程做刷新操作,可以使程序运行更加流畅。

多线程实现方案与 GCD

方案对比

16587937619902

GCD 概念介绍

任务与队列

任务:即在线程中执行的代码,在 GCD API 中以 block 的形式提交到队列
队列:任务派发队列(先进先出 FIFO,First-In-First-Out),任务追加到派发队列后按照先进先出的次序派发到对应线程进行处理

串行队列和并发队列

串行队列:队列中的任务在单个线程中顺序执行,执行中的任务结束后才能继续执行下一个任务
并发队列:队列中的任务在异步执行的情况下可以分发到多个线程同时执行
获取 队列 的方式有

注意:并发队列 的并发功能只有在异步执行(dispatch_async)下才有效。

通过 dispatch_queue_create 创建自定义的队列,通过传入参数 DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT 来区分串行队列和并发队列
系统标准提供了一个主队列(串行)和四个全局队列(并发,有优先级差异),可以通过 dispatch_get_main_queuedispatch_get_global_queue 分别获取主队列和全局队列

同步执行与异步执行

同步执行(dispatch_sync)

  • 提交任务到指定队列,在该任务执行结束之前会一直等待
  • 提交的任务只能在当前线程执行,不具备开启线程的能力

异步执行(dispatch_async)

  • 提交任务到指定队列,继续往下执行不等待任务执行结束
  • 可以在新的线程中执行任务,具备开启线程的能力

dispatch_sync 和 dispatch_async 的传入参数是队列,那同步执行、异步执行和串行队列、并发队列的组合调用分别是什么结果?
由于主队列和主线程具有一定的特殊性,以下分析当前线程为主线程的情况下,同步执行、异步执行与并发队列、串行队列、主队列的组合调用情况:
16587945499688

GCD 接口介绍

dispatch_after

使用 dispatch_after 可以实现延时执行任务的效果,需要注意的是任务会在指定延时后提交到队列,而任务真正开始执行的时间是未知的。在需要大致延时的情况下 dispatch_after 还是比较有效的
16587953775523

dispatch_group

如果希望在一个 Dispatch Queue 中所有任务执行完或者多个 Dispatch Queue 中的所有任务执行完后再执行某任务,可以通过 dispatch_group、dispatch_group_notify 实现
16587954333246
使用 dispatch_group_wait 可以设置等待 group 执行的时间上限,当 group 中全部任务执行完或者满足 timeout 条件 dispatch_group_wait 就返回,可通过返回值区分两种返回类型
16587954688585

dispatch_apply

GCD 提供了 dispatch_apply 接口用于实现快速迭代,dispatch_apply 将按照指定的次数将指定的任务追加到派发队列,并等待队列中全部任务执行结束
dispatch_apply 和 dispatch_sync 函数类似,会等待队列中的任务执行结束,在主线程中使用可能引起卡顿问题或者发生死锁,尽量在 dispatch_async 函数中非同步地执行 dispatch_apply
16587955375911

dispatch_once

某个操作在应用程序生命周期中只执行一次,这种需求很常见,比如单例模式。
dispatch_once 函数可以保证指定的处理在应用程序生命周期中只被执行一次
16587952635583

线程安全和线程同步

互斥量和信号量

互斥量(Mutex)是最简单的一种锁,在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。由哪个线程获取就由哪个线程释放,常用于临界区的互斥访问。
信号量(Semaphore)允许多个线程并发访问资源,可以实现线程同步或者控制并发访问的数量。

死锁

死锁是指多个线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁。
死锁产生的四个必要条件:

  • 互斥:资源访问是互斥的
  • 占有且等待:线程持有了资源不释放,同时请求其他资源
  • 不可抢占:其它线程不能强制夺取资源,只能由占有资源的线程主动释放
  • 循环等待:线程等待形成了环路

破坏产生死锁的四个必要条件之一可以预防死锁的发生

atomic、nonatomic

Objective-C 中定义一个类的属性时,可以指定该属性的原子性(atomic、nonatomic),默认是 atomic 的

  • atomic
    对属性 getter、setter 调用是线程安全的,需要耗费资源为属性加锁
  • nonatomic
    访问不是线程安全的,访问效率比 atomic 更高

pthread_rwlock 读写锁

OC底层->atomic与读写安全

dispatch_barrier_async

考虑多线程进行数据读写的场景,多个写操作不能并发执行,写操作和读操作不能并发执行,但是多个读操作是可以并发执行的,此时可以使用 dispatch_barrier_async 同时满足提高读操作效率和保证线程安全的要求。dispatch_barrier_async 函数会等队列中的全部任务执行结束后,再将指定的任务 X 追加到队列,之后提交的任务也需要等待 X 执行结束,仿佛 dispatch_barrier_async 给队列添加了一道“栅栏”
16587960633393

Dispatch Semaphore

Dispatch Semaphore 是 GCD 提供的信号量接口,Dispatch Semaphore 可以实现成二元信号量或者多元信号量,达到线程同步、控制并发处理数量的效果

使用信号量实现线程同步示例

使用信号量解决生产者消费者问题示例

典型场景

  • 从网络加载图片
  • 一个页面有多个请求,需要全部请求都返回的时候刷新界面
    dispatch_group + dispatch_group_notify

推荐阅读
iOS 多线程(线程的生命周期) - 掘金
iOS多线程:『GCD』详尽总结 - 掘金
iOS多线程安全-13种线程锁🔒 - 掘金