iOS 底层探究:多线程原理

这是我参与8月更文挑战的第16天,活动详情查看:[8月更文挑战]

1. 线程和进程

1.1 线程和进程的定义

线程

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程。
  • 程序启动会默认开启一条线程,这条线程被称为主线程或UI线程

进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内。
  • 通过“活动监视器”可以查看Mac系统中所开启的进程。

1.2 线程和进程的关系

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、CPU等,但是进程之间的资源是独立的。
  • 一个进程奔溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于你进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

-线程时处理器调度的基本单位,但是进程不是。

  • 线程没有地址空间,线程包含在进程地址空间中。

2. 多线程

2.1 多线程的意义

  • 优点:
    • 能适当提高程序的执行效率;
    • 能适当提高资源的利用率(CPU,内存);
    • 线程上的任务执行完成后,线程会自动销毁。
  • 缺点:
    • 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占512KB);
    • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能;
    • 线程越多,CPU在调用线程上的开销就越大,每个线程被调度的次数会降低,线程的执行效率降低;
    • 程序设计更加复杂,比如线程间的通信、多线程的数据共享。

2.2 多线程原理

对于单核CPU,同一时间,CPU只能处理一条线程,即同一时间只有一个线程在执行,iOS中的多线程同时执行的本质是CPU具有调度的能力,能够在多个任务直接进行快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。CPU在多个任务进行快速的切换的时间间隔就是时间片。
如果是多喝CPU就真的可以同时处理多个线程了,也就是并发。
多线程消耗:

image.png

2.3 多线程技术方案

方案 简介 语言 线程生命周期 使用频率
pthread 一套通用的多线程API适用于Unix/Linux/Windows等系统跨平台、可移植使用难度大 C 程序员管理 几乎不用
NSThread 使用更加面向对象简单易用,可直接操作线程对象 OC 程序员管理 偶尔使用
GCD 旨在替代NSThread等线程技术充分利用设备的多核 C 自动管理 经常使用
NSOperation 基于GCD(底层是GCD)比GCD多了一些更简单实用的功能使用更加面向对象 OC 自动管理 经常使用
### 2.4 多线程生命周期
线程的五个状态
  • 新建:只要是实例化线程对象。
  • 就绪:线程对象调用start方法,将线程对象加入可调度线程池,等待CPU的调用,即调用start方法,并不会立即执行,进入就绪状态,需要等待一段时间,经CPU调度后才执行,也就是从就绪状态进入运行状态。
  • 运行:CPU负责调度可调度线程池中线程的执行,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责,开发人员不能干预。
  • 阻塞:当满足某个预定条件时,可以使用休眠,即sleep,或者同步锁,阻塞线程执行。当sleep到时,会获取同步锁重新将线程加入可调度线程池中。
  • 死亡:氛围两种情况
    • 1.正常死亡:即线程执行完毕。
    • 2.非正常死亡:当满足某个条件后,在线程内部(或者中线程中)中止执行(调用exit方法等退出)。

image.png

2.5 线程池原理

image.png
饱和策略

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

这四种拒绝策略均实现了RejectedExecutionHandler接口

3.问题

3.1 任务的执行速度的影响因素

  • cpu
  • 任务复杂度
  • 优先级
  • 线程状态

3.2 优先级反转

在看优先级反转前先了解什么是IO密集型线程和CPU密集型线程。

  • IO密集型线程:频繁等待的线程,等待的时间会让出时间片。
  • cpu密集型线程:很少等待的线程,意味着长时间占用着CPU。

IO密集型线程比CPU密集型线程更容易得到优先级提升。
特殊场景下,当多个优先级都比较高的CPU密集型线程霸占了所有CPU资源,而此时优先级较低的IO密集型线程将持续等待,产生线程饿死的现象。当然为了避免线程饿死,CPU会发挥调度作用去逐步提高被“冷落”线程的优先级(提高优先级并不一定会立即执行),IO密集型线程通常情况下比CPU密集型线程更容易获取到优先级提升。
线程的优先级影响因素:

  • 用户指定线程的服务质量
  • 根据线程等待的频繁程度提高或者降低
  • 长时间不执行的线程,提升它的优先级

用户指定线程优先级:

image.png

image.png

4.线程的安全问题

多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个过程中如果没有锁机制往往会造成重大问题。
iOS主要有两种锁:

  • 互斥锁
  • 自旋锁

4.1 互斥锁

  • 用于保护临界区,确保同一时间,只有一条线程能够执行
  • 如果代码中只有一个地方需要加锁,大多都使用self,这样可以避免单独再创建一个锁对象
  • 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠

使用互斥锁的注意事项:

  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差
  • 能够加锁的任意NSObject对象
  • 锁对象一定要保证所有的线程都能够访问

4.2 自旋锁

  • 自旋锁于互斥锁蕾丝,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态
  • 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本省就是一把自旋锁
  • 加入了自旋锁,当新线程访问代码时,如果发现其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能。

4.3 自旋锁于互斥锁的异同点

相同点:

  • 在同一时间,保证只有一条线程执行任务,即保证了相应同步的功能

不同点:

  • 互斥锁:发现其他线程执行,当前线程休眠(即就绪状态),进入等待执行,即挂起。一直等其他线程打开之后,然后唤醒执行
  • 自旋锁:发现其他线程执行,当前线程一直询问(即一直访问),处于忙等状态,耗费的性能比较高

使用场景:

  • 根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理

  • 当前的任务状态比较短小精悍时,用自旋锁

  • 反之的,用互斥锁

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享