OC底层原理25:多线程原理探索
前言
本文的目的在于了解 进程、线程、多线程、线程池
等的基本概念及原理
线程和进程的定义
线程
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
- 进程要想执行任务,必须得有线程,
一个进程至少要有一条线程
程序启动会默认开启一条线程
,这条线程被称为主线程
或者UI线程
进程
进程
是指在系统中正在运行的一个应用程序
- 每个
进程之间是独立
的,每个进程均运行在其专用的且受保护的内存空间内
- 通过
活动监视器
可以查看Mac
系统中所开启的线程
所以,可以简单的理解为:进程是线程的容器,而线程是用来执行任务的
。在iOS中是单进程开发,一个进程就是一个APP
,进程
之间是相互独立的
,如:微信、支付宝、qq等,这些都属于不同的进程。
进程和线程的关系
进程和线程之间的关系主要涉及两个方面:
地址空间
- 同一个进程的
线程共享本进程的地址空间
- 而
进程之间则是独立的地址空间
资源拥有
- 同一个进程内
线程共享进程的资源
,如内存、I/O、cpu等 - 但是
进程之间资源是独立的
进程和线程的关系就相当于工厂和流水线的关系
,工厂和工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即进程
相当于一个工厂
,线程
相当于工厂中的一条流水线
针对进程和线程,还有以下几点说明:
1、多进程要比多线程健壮
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响
- 而一个线程崩溃后整个进程都死掉
2、使用场景:频繁切换、并发操作
- 进程切换时,小号的资源大、效率高,所以设计到
频繁切换
时,使用线程要好与进程 - 同样,如果要求同时进行并又要共享某些变量的
并发操作
,只能用线程
,不能用进程
3、执行过程
- 每个独立的
进程
有一个程序运行的入口
、顺序执行序列
和程序入口
- 但是
线程不能独立执行,必须依赖存在应用程序中
,由应用程序提供多个线程来执行控制
4、线程是处理器调度的基本单元
,但进程不是
5、线程没有地址空间,线程包含在进程的地址空间中
线程 和 Runloop 的关系
runloop与线程是一一对应的
,一个runloop
对应一个核心
的线程,为什么说核心的
,是因为runloop是可以嵌套的,但是核心的只能有一个
,他们的关系保存在一个全局的字典里
runloop是用来管理线程的
,当线程的runloop
被开启后,线程会在执行完任务后进入休眠状态
,有了任务就会被唤醒去执行任务。runloop
在第一次获取时被创建,在线程结束时被销毁对于
主线程
来说,runloop在程序已启动就默认创建好了
对于
子线程
来说,runloop是来加载的,只有当我们使用的时候才会创建
,所以在子线程使用定时器
要注意,确保子线程的runloop被创建,不然定时器不会回调
多线程
多线程原理
- 对于
单核CPU
,同一时间,CPU只能处理一条线程,即只有一条线程在工作 - iOS中的多线程同时执行,其本质是
CPU在多个任务间直接进行快速切换
,由于CPU调度线程的时间足够快,就造成了多线程同时执行
的效果,其中奇幻的时间间隔就是时间片
多线程意义
优点
- 能适当
提高程序的执行效率
- 能适当
提高资源的利用率
,如CPU、内存 - 线程上的任务执行完成后,
线程会自动销毁
缺点
开启线程需要占用一定的内存空间
,默认情况下,每一个线程占用512KB
- 如果开启
大量线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调用线程上的开销越大
- 程序设计更加复杂,比如线程间的通信,多线程的数据共享
多线程生命周期
多线程的声明周期主要分为五个部分:新建-就绪-运行-阻塞-死亡
,如下图所示:
新建
:主要是实例化线程对象就绪
:线程对象调用start方法
,将线程对象加入可调度线程池
,等待CPU的调用
,即调用start方法,并不会立即执行
,进入就绪状态
,需要等待一段时间,经CPU调度后才执行
,也就是从就绪状态进入运行状态
运行
:CPU负责调度可调度线程的执行
,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责
,开发人员不能干预阻塞
:当满足某个预定条件时,可以使用休眠、即sleep、或者同步锁
,阻塞线程执行,当进入sleep时,会重新将线程加入就绪
中,下面关于休眠的时间设置,都是NSThread
的sleepUnilDate
:阻塞当前线程,直到指定的时间为止,即 `休眠到指定时间sleepForTimeInerval
:在给定的时间间隔内休眠线程,即指定休眠时长
- 同步锁:
@synchronized(self)
死亡
:分为两种情况正常死亡
,即线程执行完毕非正常死亡
,即当满足某个条件后,在线程内部(或主线程中)终止执行(调用exit方法等退出)
简要说明:就是处于
运行中的线程
拥有一段可以执行的事件(成为时间片
)- 如果
时间片用尽
,线程就会进入就绪状态队列
- 如果
时间片没有用尽
,且需要开始等待某事件
,就会进入阻塞状态队列
- 等待事件发生后,线程又重新进入
就绪状态队列
- 每当一个
线程离开运行
,即执行完毕或者强制退出后,会重新从就绪状态队列中
选择一个线程继续执行
- 如果
线程的 exit
和 cancel
说明:
exit
:一旦强行终止线程,后续的所有代码都不会执行
1 | - cancel:取消当前线程,但是不能取消正在执行的线程 |
【面试题】线程的优先级越高,是不是意味着任务的执行越快?
答:并不是
,线程执行的快慢,除了要看优先级,还需要查看 资源的大小(即任务的复杂度)
、以及 CPU调度
情况,在NSThread中,线程优先级 threadPriority
已经被服务质量 qualityOfService
取代,一下是相关的枚举值:
线程池的原理
【第一步】判断核心线程是否都正在执行任务
- 返回NO,创建新的工作线程去执行
- 返回YES,进入【第二步】
【第二步】判断线程池工作队列是否已经饱满
- 返回NO,将任务存储到工作队列,等待CPU调度
- 返回YES,进入【第三步】
【第三步】判断线程池中的线程是否都出于执行状态
- 返回NO,安排可调度线程池中空闲的线程去执行任务
- 返回YES,进入【第四步】
【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4中策略)
AbortPolicy
:直接抛出RejectedExecutionExeception
异常来阻止系统正常运行CallerRunsPolicy
:将任务回退到调用者DisOldestPolicy
:丢掉等待最久的任务DisCardPolicy
:直接丢弃任务
iOS中多线程的实现方案
iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation
,汇总如图所示:
方案 | 简介 | 语言 | 线程的声明周期 | 使用频率 |
---|---|---|---|---|
Pthread | 1、一套通用的多线程API 2、使用与Unix/Linus/windows等系统 3、跨平台、可移植 4、使用难度大 | C | 程序员管理 | 几乎不用 |
NSThread | 1、使用更加面向对象 2、使用简单,可直接操作线程对象 | OC | 程序员管理 | 偶尔使用 |
GCD | 1、旨在替代GCD等线程技术 2、充分利用设备的多核 | C | 自动管理 | 经常使用 |
NSOperation | 1、基于GCD(底层是GCD) 2、比GCD多了一些更简单实用的功能 3、使用更加面向对象 | OC | 自动管理 | 经常使用 |
下面是四种方案的简单示例:
1 | // *********1: pthread********* |
C 和 OC 的桥接
其中涉及C与OC的桥接,有以下几点说明
__bridge
只做类型转换,但是不修改对象(内存)管理权
__bridge_retained
(也可以使用CFBridgingRetain
)将Object-C的对象
转换为Core Foundation的对象
,同时将对象(内存)的管理权交给我们
,后续需要使用 CFRelease或相关方法来释放对象
__bridge_transfer
(也可以使用CFBridgingRelease
)将Core Foundation
的对象 转换为Object-C的对象
,同时将对象(内存)的管理权交给ARC
线程安全问题
当多个线程同时访问一块内存资源时,容易引发数据错乱 和 数据安全问题,有以下两种解决方案:
- 互斥锁(即同步锁):
@synchronized
- 自旋锁
互斥锁
- 用于保护临界区,确保
同一时间,只有一条线程能够执行
- 如果代码中
只有一个地方需要加锁,大多数都使用 self
,这样可以避免单独再创建一个锁对象 - 加入互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入
休眠
针对互斥锁,还需要注意以下几点:
- 互斥锁的
锁定范围,应该尽量小
,锁定范围越大,效率越差 - 能够
加锁的任意 NSObject 对象
- 锁对象一定要保证所有的线程都能够访问
自旋锁
自旋锁
与互斥锁
类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,成为自旋)阻塞状态
- 使用场景:锁持有的时间短,切线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符
atomic
,本身就有一把自旋锁
- 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用
死循环
的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能
【面试题】:自旋锁 vs 互斥锁
同:在同一时间,保证了只有一条线程执行任务,即保证了相应
同步
的功能不同:
互斥锁
:发现其他线程执行,当前线程休眠(即就绪状态)
,进入等待执行,即挂起,一直等其他线程打开后,然后唤醒执行自旋锁
:发现其他线程执行,当前线程一直询问
(即一直访问),处于忙等状态,耗费的性能
比较高
场景:根据任务复杂度区分,使用不同的锁,但判断补全时,更多的是使用
互斥锁
去处理- 当前的任务状态比较
短小精悍
时,用自旋锁
- 反之的,用互斥锁
- 当前的任务状态比较
atomic 原子锁 & nonatomic 非原子锁
atomic
和 nonatomic
主要用于属性的修饰,以下是相关的一些说明:
atomic
是原子属性
,是为多线程开发准备
的,是默认属性- 仅仅在属性的
setter
方法中,增加了锁(自旋锁)
,能够保证同一时间,只有一条线程
对属性进行写
操作 同一时间单(线程)写多(线程)读
的线程处理技术
- Mac开发中常用
- 仅仅在属性的
nonatomic
是非原子属性
没有锁
,性能高
- 移动端开发常用
【面试题】 atomic 和 nonatmic区别
nonatomic
非原子属性
非线程安全
,适合内存小的移动设备
atomic
原子属性
,线程安全
,针对多线程设计的,默认值- 保证
同一时间只有一个线程能够写入
,但是同一个时间多个线程都可以取值 - atomic本身就有一把锁(
自旋锁
),单写多读
:单个线程写入,多个线程读取 线程安全
,需要消耗大量的资源
iOS开发建议
- 所有
属性
都声明为nonatomic
- 尽量避免多线程抢夺同一块资源,尽量
将加锁、资源抢夺的业务逻辑交给服务器处理,减小移动端客户端的压力你
线程间的通讯
在Threading Programming Guide 文档中,提及,线程间的通讯有以下几种方式
直接消息传递
:通过performSelector
的一系列方法,可以实现由某一线程指定在另外的线程上执行任务,因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化全局变量、共享内存块和对象
:在两个线程之间传递信息的另一种简单的方法是使用全局变量
,共享对象或共享内存块,尽管共享变量即快速又简单,但是它们比直接消息传递更脆弱,必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性
,否则可能会导致竞争状况,数据损坏或崩溃。条件执行
: 条件是一种同步工具
,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。Runloop sources
: 一个自定义的Runloop source
配置可以让一个线程上收到特定的应用程序消息。由于Runloop source 是事件驱动
的,因此在无事可做时,线程会自动进入睡眠状态
,从而提高了线程的效率Ports and sockets
:基于端口的通信
是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术
。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用Runloop source 来实现端口
,因此当端口上没有数据等待时,线程将进入睡眠状态。需要注意的是,端口通讯需要将端口加入到主线程的Runloop中
,否则不会走到端口回调方法消息队列
: 传统的多处理服务定义了先进先出(FIFO)
队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效Cocoa 分布式对象
: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高
- Post title:OC底层原理25:多线程原理探索
- Post author:张建
- Create time:2021-02-21 09:35:17
- Post link:https://redefine.ohevan.com/2021/02/21/OC底层原理/OC底层原理25:多线程原理探索/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.