Objective-C 多线程编程与 GCD | 青训营笔记

99 阅读7分钟

1. 多线程基础概念

这是我参与「第四届青训营」笔记创作活动的的第5天

1.1 进程与线程

  • 进程:是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
  • 线程:一个进程可以包含多个线程,线程是操作系统实施调度的最小单位

1.1.1 进程与线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位(也可以理解为进程当中的一条执行流程)
  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
  • 调度和切换:线程上下文切换比进程上下文切换要快得多。

1.2 串行、并行、并发

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

image.png

1.3 周期和线程调度

线程的生命周期:包括新建、就绪、运行、阻塞、死亡五种状态

线程调度:当在单个处理器上运行多个线程,操作系统会让这些线程轮流执行一小段时间,宏观上“看起来”这些线程是同时执行的。不断地在处理器上切换不同线程的行为称为线程调度

image.png

1.4 RunLoop

RunLoop 是事件接收和分发机制的一个实现,可以让线程在适当的时间处理任务不会退出。iOS App中,主线程的 RunLoop 在程序运行时就会启动

RunLoop 的基本作用:保持程序持续运行、处理程序中的各种事件、节省 CPU 资源提高程序性能,在必要的时候线程会被唤醒进行工作,否则会进入休眠,休眠时不占用CPU。

1.5 主线程

主线程: iOS App 启动时默认会开启一条线程,称为“主线程”。主线程默认开启 RunLoop,使得主线程可以及时刷新 UI 界面和处理 UI 交互事件(如点击、滑动、拖拽等),所以主线程又称为“UI 线程”。

耗时操作会妨碍主线程中的主循环的执行,从而引起 App 卡顿问题。通过多线程编程,将耗时操作放到子线程执行,在必要时再回到主线程做刷新操作,可以使程序运行更加流畅。

2 GCD

2.1 GCD好处

GCD:Grand Central Dispatch

好处

  • 自动使用更多 CPU 内核
  • 自动管理线程的生命周期
  • 提供了易于使用的并发模型
  • 编码更加简洁,无需多余的线程管理任务

2.2 GCD 接口介绍

2.3 任务与队列

任务:即在线程中执行的代码,在 GCD API 中以 block 的形式提交到队列

队列:任务派发队列(先进先出FIFO,First-In-First-Out),任务追加到派发队列后按照先进先出的次序派发到对应线程进行处理

2.4 串行队列和并发队列

串行队列:队列中的任务在单个线程中顺序执行,执行中的任务结束后才能继续执行下一个任务

说明这个队列中的任务要串行执行,也就是一个一个的执行,必须等上一个任务执行完成之后才能开始下一个,而且一定是按照先进先出的顺序执行的,比如串行队列里面有4个任务,进入队列的顺序是a、b、c、d,那么一定是先执行a,并且等任务a完成之后,再执行b... 。

并发队列:队列中的任务在异步执行的情况下可以分发到多个线程下同时执行

说明这个队列中的任务可以并发执行,也就任务可以同时执行,比如并发队列里面有4个任务,进入队列的顺序是a、b、c、d,那么一定是先执行a,再执行b...,但是执行b的时候a不一定执行完成,而且a和b具体哪个先执行完成是不确定的, 具体同时执行几个,由系统控制(GCD中不能直接设置并发数,可以通过创建信号量的方式实现,NSOperationQueue可以直接设置),但是肯定也是按照先进先出(FIFO, First-In-First-Out)的原则调用的。

2.5 同步执行与异步执行

同步执行(dispatch_sync):

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

异步执行(dispatch_async):

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

image.png

2.6 dispatch_after

使用 dispatch_after可以实现延时执行任务的效果

2.7 dispatch_group

如果希望在一个 Dispatch Queue 中所有任务执行完或者多个 Dispatch Queue 中的所有任务执行完后再执行某任务,可以通过dispatch_groupdispatch_group_notify 实现

2.8 dispatch_apply

实现快速迭代,dispatch_apply 将按照指定的次数将指定的任务追加到派发队列,并等待队列中全部任务执行结束

2.9 dispatch_once

指定的处理在应用程序生命周期中只被执行一次

3 线程安全和线程同步

3.1 锁

互斥量(Mutex)  是最简单的一种锁,在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。由哪个线程获取就由哪个线程释放,常用于临界区的互斥访问。

信号量(Semaphore)  允许多个线程并发访问资源,可以实现线程同步或者控制并发访问的数量。

3.2 死锁

死锁产生的四个必要条件

互斥条件:资源访问是互斥的

占有且等待条件:线程持有了资源不释放,同时请求其他资源

不可抢占条件:其它线程不能强制夺取资源,只能由占有资源的线程主动释放

循环等待条件:线程等待形成了环路

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

3.3 atomic、nonatomic

atomic

  • 对属性 getter、setter 调用是线程安全的
  • 需要耗费资源为属性加锁

nonatomic

  • 访问不是线程安全的
  • 访问效率比 atomic 更高

3.4 dispatch_barrier_async

栅栏函数: dispatch_barrier_async 函数会等队列中的全部任务执行结束后,再将指定的任务 X 追加到队列,之后提交的任务也需要等待 X 执行结束

3.5 Dispatch Semaphore

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

4 小结

这节课学习了GCD相关内容,GCD高效易用,在开发过程中很重要,尤其是一次性函数可用于实现单例模式相关内容,把耗时操作不放在主线程也是基本操作

5 引用参考

Objective-C 多线程编程与 GCD