前言
在开发中,由于一些业务需求,我们不得不使用多线程来实现。那么多线程的原理是怎样的呢,和进程又是怎样的关系?本文将对多线程进行归纳总结下。
线程和进程
线程
线程是
进程的基本执行单元
,一个进程的所有任务都在线程中执行进程想要执行任务,必须至少有一条线程
程序启动会默认开启一条线程,也就是我们常说的
主线程
也称为UI线程
进程
进程是指在系统中
正在运⾏的⼀个应⽤程序
,就是一段程序的执行过程,可以理解为手机上的一个app
每个进程之间是独立的,每个进程均运行在其
专用且受保护的内存空间内
通过
活动监视器
可以查看MAC
系统中所有开启的进程
多线程的意义
优点:
能适当
提高程序的执行效率
能适当
提高资源的利用率(CPU,内容)
线程上的
任务执行完成后
,线程会自动销毁
缺点:
开启线程需要
占用一定的内存空间
(默认情况下
,每一个线程都占512KB
)如果开启
大量的线程
,会占用大量的内存空间
,降低程序的性能
线程越多
,CPU在调用线程上的开销就越大
多线程使
程序设计更加复杂
,比如线程间的通信、多线程的数据共享
多线程的原理
多线程是CPU
在单位时间的时间片内
来回调度
和切换
,构成了很多任务同时进行的假象
单线程:
(单核
CPU
)同一时间,CPU
只能处理一个线程
,换言之就是同一时间只有一个线程在执行
多线程:
CPU
快速的在多个线程之间的切换,CPU
调度线程的时间足够快,就造成了多线程的同时
执行效果
如果线程数非常多:
CPU
会在N
个线程之间切换,消耗大量的CPU
资源,每个线程被调度的次数就降低
线程的生命周期
多线程时CPU
在多个线程之间来回切换,也就是有状态之分,当一个线程在被调度时,其他线程是的状态怎样的呢?于是就引出了线程的生命周期
线程的生命周期
分为5步:新建
->就绪
->运行
->阻塞
->死亡
,如下图:
新建:
使用
new
实例化一个线程对象,但该线程对象还未
使用start()
方法启动线程这个阶段,该阶段只在内存的堆
中为该对象的实例变量分配了内存空间
,但线程还无法
参与抢夺CPU的使用权
。
就绪:
是指一个线程对象使用
start()
方法将线程加入可调度线程池
后就进入就绪阶段,等待CPU来调度
后才会执行
运行:
当
CPU
开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。线程要想进入运行状态执行,首先必须先处于处于就绪状态中。运行状态可能和就绪状态来回切换,这个和CPU调度
有关。
阻塞:
处于运行状态中的线程由于某种原因例如(调用sleep、等待同步锁、从可调度线程池移出等),此时进入阻塞状态。
例如:
sleepForTimeInterval
,sleepUntilDate
函数,同步锁synchronized
等可使线程进入阻塞状态
死亡:
正常死亡:线程执行完毕。
非正常死亡:当线程因异常而退出,或者调用
exit
可调度线程池
工作原理是:
如果都在执行状态,则交给
饱和策略处理
。
如果饱满,则会判断线程是否都处于执行状态,如果没有,则安排非核心线程去执行
如果都在执行,就会检查工作队列是否饱满,如果未饱满,就会将任务存储在工作队列
有新任务来时,会先判断线程池是否都在执行任务,如果
没到就会创建线程执行任务
饱和策略
饱和策略主要有4种:
DisCardPolicy
:直接丢弃任务
DisOldestPolicy
:丢掉等待最久的任务
CallerRunsPolicy
:将任务回退到调用者
AbortPolicy
:直接抛出RejectedExecutionExeception
异常来阻止系统正常运行
这四种拒绝策略均由RejectedExecutionHandler
接口实现
任务执行的影响因素
任务执行的影响因素通常有4种:
CPU
任务的复杂度
优先级
线程状态
举例
就例如排两队买奶茶:
如果只有一个服务员,肯定速度比较慢,如果有两个,每个人给一队处理,那就明显快很多,这就相当于
cpu
如果一队第一个人之买一杯,另一队正在买的人要十杯,那肯定一杯的先买完,也就是
任务复杂度
如果两队的第一人都在买,且都买一杯,但其中一个是
VIP
,那就VIP
优先处理,也就是优先级
如果其中一个服务员,不小心将即将处理好的奶茶打翻了,或者临时接了一个电话,那么也影响了速度,这个就是
线程状态
因素
优先级
IO密集型
的线程特点是频繁等待,而CPU密集型
则很少等待,所以CPU密集型
的优先级要高,但优先级提高后也不一定执行,只是比低优先级的线程更可能运行,详情请查看 Threading Programming Guide
可以通过
NSThread
中的setThreadPriority:
,或者POSIX
的pthread_setschedparam
方法来设置优先级
线程安全
在使用多线程时,往往会出现资源抢夺问题,这时就需要加锁
来解决线程安全的问题,也就是将各个线程对同一个数据的访问同步(Synchronization)
。所谓同步,即指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问,如此,对数据的访问被原子化
了。下面主要介绍:自旋锁
、互斥锁
自旋锁
自旋锁
是计算机科学用于多线程同步
的一种锁,线程反复检查
锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等(忙碌等待)
。一旦获取了自旋锁,线程会一直持有
该锁,直至显式释放
自旋锁。
互斥锁
保证锁内的代码在
同一时间只有一条线程
能够执行
互斥锁的
锁定范围应该尽量小
,锁定范围越大效率越差
锁对象
一定要保证所有的线程都能够访问
,如果代码中只有一个地方需要加锁大多都是用self
,这样可以避免单独创建一个锁对象
互斥锁
能够加锁任意NSObject对象
业务场景:1. 当任务比较简单比较小的任务时,选择
自旋锁
. 2. 当任务比较庞大繁琐时,选择互斥锁
在使用属性时,我们用到了atomic
和nonatomic
,一个是线程安全的,另一个是不安全的,那么他们是加锁了么,接下来去研究下
atomic和nonatomic
nonatomic
是非原子属性,非线程安全,适合内存小的移动设备atomic
是原子属性(线程安全)
,针对多线程设计的默认值,保证同一时间只有一个线程能够写入
,但是同一个时间多个线程都可以取值
,这个特性也就是多读单写
atomic
本身就有一把锁(自旋锁
)我们知道属性在底层会去查找
setter
和getter
方法,在objc4-812
源码中搜索objc_setProperty
:
核心方法是reallySetProperty
方法,然后进入查看:
当
atomic
时,会调用PropertyLocks
加一个spinlock_t
类型的自旋锁,所以atomic
是一个bool
值不是锁iOS
开发的建议所有的属性都声明为
nonatomic
尽量避免多线程抢夺同一资源
尽量讲加锁、资源抢夺的业务逻辑交给服务端处理,减小移动客户端的压力
线程和runloop关系
runloop
与线程是⼀⼀对应的,⼀个runloop
对应⼀个核⼼的线程,为什么说是核⼼的,是因为runloop
是可以嵌套的,但是核⼼的只能有⼀个,他们的关系保存在⼀个全局的字典⾥
runloop
是来管理线程的,当线程的runloop
被开启后,线程回在执行完任务后进入休状态,有了任务就会被唤醒去执行任务
runloop
在第一次获取时被创建,在线程结束时被销毁
对于
主线程
来说,runloop
在程序一启动就默认创建好了
对于子线程来说,
runloop
是懒加载的
,只有当我们使用的时候才会创建,所以在子线程时用定时器要注意:确保子线程的runloop
被创建,不然定时器就不会回调
多线程的实现方案
iOS
多线程方案如下:
作者:无双3