手记

iOS底层-多线程原理

前言

在开发中,由于一些业务需求,我们不得不使用多线程来实现。那么多线程的原理是怎样的呢,和进程又是怎样的关系?本文将对多线程进行归纳总结下。

线程和进程

  • 线程

    • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行

    • 进程想要执行任务,必须至少有一条线程

    • 程序启动会默认开启一条线程,也就是我们常说的主线程也称为UI线程

  • 进程

    • 进程是指在系统中正在运⾏的⼀个应⽤程序,就是一段程序的执行过程,可以理解为手机上的一个app

    • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内

    • 通过活动监视器可以查看MAC系统中所有开启的进程

多线程的意义

  • 优点:

    • 能适当提高程序的执行效率

    • 能适当提高资源的利用率(CPU,内容)

    • 线程上的任务执行完成后,线程会自动销毁

  • 缺点:

    • 开启线程需要占用一定的内存空间默认情况下,每一个线程都占512KB

    • 如果开启大量的线程,会占用大量的内存空间降低程序的性能

    • 线程越多,CPU在调用线程上的开销就越大

    • 多线程使程序设计更加复杂,比如线程间的通信、多线程的数据共享

多线程的原理

多线程是CPU单位时间的时间片内来回调度切换,构成了很多任务同时进行的假象

  • 单线程

    • (单核CPU)同一时间,CPU只能处理一个线程,换言之就是同一时间只有一个线程在执行

  • 多线程

    • CPU快速的在多个线程之间的切换,CPU调度线程的时间足够快,就造成了多线程的同时执行效果

  • 如果线程数非常多

    • CPU会在N个线程之间切换,消耗大量的CPU资源,每个线程被调度的次数就降低

线程的生命周期

多线程时CPU在多个线程之间来回切换,也就是有状态之分,当一个线程在被调度时,其他线程是的状态怎样的呢?于是就引出了线程的生命周期

  • 线程的生命周期分为5步:新建 -> 就绪 -> 运行 -> 阻塞 -> 死亡,如下图:


  • 新建:

    • 使用new实例化一个线程对象,但该线程对象还未使用start()方法启动线程这个阶段,该阶段只在内存的中为该对象的实例变量分配了内存空间,但线程还无法参与抢夺CPU的使用权

  • 就绪:

    • 是指一个线程对象使用start()方法将线程加入可调度线程池后就进入就绪阶段,等待CPU来调度后才会执行

  • 运行:

    • CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。线程要想进入运行状态执行,首先必须先处于处于就绪状态中。运行状态可能和就绪状态来回切换,这个和CPU调度有关。

  • 阻塞:

    • 处于运行状态中的线程由于某种原因例如(调用sleep、等待同步锁、从可调度线程池移出等),此时进入阻塞状态。

    • 例如:sleepForTimeIntervalsleepUntilDate函数,同步锁synchronized等可使线程进入阻塞状态

  • 死亡:

    • 正常死亡:线程执行完毕。

    • 非正常死亡:当线程因异常而退出,或者调用exit

可调度线程池


  • 工作原理是:





  1. 如果都在执行状态,则交给饱和策略处理

  1. 如果饱满,则会判断线程是否都处于执行状态,如果没有,则安排非核心线程去执行

  1. 如果都在执行,就会检查工作队列是否饱满,如果未饱满,就会将任务存储在工作队列

  1. 有新任务来时,会先判断线程池是否都在执行任务,如果没到就会创建线程执行任务

饱和策略

  • 饱和策略主要有4种:





  1. DisCardPolicy:直接丢弃任务

  1. DisOldestPolicy:丢掉等待最久的任务

  1. CallerRunsPolicy:将任务回退到调用者

  1. AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行

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

任务执行的影响因素

任务执行的影响因素通常有4种:

  • CPU

  • 任务的复杂度

  • 优先级

  • 线程状态

举例

就例如排两队买奶茶:

  • 如果只有一个服务员,肯定速度比较慢,如果有两个,每个人给一队处理,那就明显快很多,这就相当于cpu

  • 如果一队第一个人之买一杯,另一队正在买的人要十杯,那肯定一杯的先买完,也就是任务复杂度

  • 如果两队的第一人都在买,且都买一杯,但其中一个是VIP,那就VIP优先处理,也就是优先级

  • 如果其中一个服务员,不小心将即将处理好的奶茶打翻了,或者临时接了一个电话,那么也影响了速度,这个就是线程状态因素

优先级

IO密集型的线程特点是频繁等待,而CPU密集型则很少等待,所以CPU密集型的优先级要高,但优先级提高后也不一定执行,只是比低优先级的线程更可能运行,详情请查看 Threading Programming Guide


  • 可以通过NSThread中的setThreadPriority:,或者POSIXpthread_setschedparam方法来设置优先级

线程安全

在使用多线程时,往往会出现资源抢夺问题,这时就需要加锁来解决线程安全的问题,也就是将各个线程对同一个数据的访问同步(Synchronization)。所谓同步,即指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问,如此,对数据的访问被原子化了。下面主要介绍:自旋锁互斥锁

自旋锁

  • 自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等(忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。

互斥锁


  1. 保证锁内的代码在同一时间只有一条线程能够执行


  1. 互斥锁的锁定范围应该尽量小,锁定范围越大效率越差


  • 锁对象一定要保证所有的线程都能够访问,如果代码中只有一个地方需要加锁大多都是用self,这样可以避免单独创建一个锁对象

  1. 互斥锁能够加锁任意NSObject对象

业务场景:1. 当任务比较简单比较小的任务时,选择自旋锁. 2. 当任务比较庞大繁琐时,选择互斥锁

在使用属性时,我们用到了atomicnonatomic,一个是线程安全的,另一个是不安全的,那么他们是加锁了么,接下来去研究下

atomic和nonatomic

  • nonatomic是非原子属性,非线程安全,适合内存小的移动设备

  • atomic是原子属性(线程安全),针对多线程设计的默认值,保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以取值,这个特性也就是多读单写

  • atomic本身就有一把锁(自旋锁)

  • 我们知道属性在底层会去查找settergetter方法,在objc4-812源码中搜索objc_setProperty


核心方法是reallySetProperty方法,然后进入查看:


  • atomic时,会调用PropertyLocks加一个spinlock_t类型的自旋锁,所以atomic是一个bool不是锁

  • iOS开发的建议

    • 所有的属性都声明为nonatomic

    • 尽量避免多线程抢夺同一资源

    • 尽量讲加锁、资源抢夺的业务逻辑交给服务端处理,减小移动客户端的压力

线程和runloop关系


  1. runloop与线程是⼀⼀对应的,⼀个runloop对应⼀个核⼼的线程,为什么说是核⼼的,是因为runloop是可以嵌套的,但是核⼼的只能有⼀个,他们的关系保存在⼀个全局的字典⾥


  1. runloop是来管理线程的,当线程的runloop被开启后,线程回在执行完任务后进入休状态,有了任务就会被唤醒去执行任务


  1. runloop在第一次获取时被创建,在线程结束时被销毁


  1. 对于主线程来说,runloop在程序一启动就默认创建好了


  1. 对于子线程来说,runloop懒加载的,只有当我们使用的时候才会创建,所以在子线程时用定时器要注意:确保子线程的runloop被创建,不然定时器就不会回调

多线程的实现方案

iOS多线程方案如下:


作者:无双3


0人推荐
随时随地看视频
慕课网APP