iOS 多线程之基础
在开始之前,先理解以下几点
一、进程和线程的区别和联系:
1、地址资源:进程有自己的内存地址,进程内的线程可以共享进程的内存地址
2、资源分配和调度:进程是系统进行资源分配和拥有的基本单位,同一个进程内的线程可共享进程的资源
3、线程是CPU调度的基本单位。
4、二者都可以并发执行
二、多线程的意义
优点:
1、提高资源(CPU和内存)利用率
2、提高程序的执行效率
3、线程上的任务执行完后,线程会自动销毁
缺点:
1、开辟线程需要占用一定的内存空间(默认每个线程占用512kb)
2、线程越多,CPU调度的开销越大
三、多线程原理
多线程的并发执行其实并不是同时执行,而是CPU在不同的线程间频繁切换,达到的“伪同时”效果。这是由于每一个分得CPU的任务都会有一个时间片,它执行完时间片的时间,CPU就不属于它们了,要等待再次分配。
抛出问题
问题1、主线程是做什么的?
答:主线程是iOS程序运行后开辟的第一个线程,也叫UI线程,用来显示/刷新UI界面和处理UI事件。
问题2、UI为什么要在主线程更新?
答:苹果为了性能考虑,UIKit不是线程安全的,试想如果UI可以在子线程更新,那么如果有多个线程同时修改某个资源时将会出现很多莫名其妙的错误。
问题3、能不能把渲染放到子线程?怎么渲染
遗留问题。
线程和Runloop的关系
1、Runloop和线程是一一对应的关系
2、Runloop是来管理线程的,当线程的runloop被开启后,线程执行完任务会休眠,等下次有任务时再执行任务。
3、线程在第一次创建是被开启,在线程结束时销毁
3、Runloop在子线程中默认不开启,需要手动操作才能开启。注意NSTImer
线程池工作原理
系统中的线程属于宝贵资源,合理运用线程池,可以减少创建和销毁线程的次数,使得每个线程可以重复利用。
- corePoolSize:线程池基本大小(核心线程数量)
- maximumPoolSize:线程池允许的最大线程数
- keepAliveTime:当线程池的线程数量大于corePoolSize时,空闲线程的最大存活时间
- workQueue:存放任务的工作队列
- handler:超出线程范围和队列容量的任务的处理程序
线程池的工作流程:当提交一个任务时,线程池会做出如下判断
1、如果正在运行的线程数量小于corePoolSize,马上创建线程执行任务。
2、如果正在运行的线程数量大于等于corePoolSize,则放入工作队列中。
3、如果不巧,工作队列也满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程立刻运行这个任务。
4、如果正在运行的线程数量大于等于maximumPoolSize,则交给饱和策略处理。
如图所示
当线程池的线程执行完任务以后,会从工作队列取下一个任务执行。
当一个线程空闲,且正在运行的线程数大于corePoolSize,则计时超过keepAliveTime线程就会被kill掉,所以当线程池中任务都完成后,它最终会缩小到corePoolSize大小。
饱和策略
当任务提交线程池失败时,即当前提交的任务数超过maxmumPoolSize
与workQueue
之和时,就会交给饱和策略处理。
饱和策略有四种,AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
、DiscardOldestPolicy
。
策略 | Description |
---|---|
AbortPolicy | 中止策略,属于默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需要编写自己的处理代码。 |
DiscardPolicy | 抛弃策略,当新提交的任务无法保存到队列中等待执行时,“抛弃(Discard)”策略会悄悄抛弃该任务。 |
DiscardOldestPolicy | 抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”饱和策略和优先队列放在一起使用。) |
CallerRunsPolicy | 调用者策略,策略实现了一种调度机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。如果采用有界队列和“调用者运行”饱和策略,当线程池中的所有线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程中执行。由于执行任务需要一定的时间,因此主线程至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理完正在执行的任务。在此期间,主线程不会调用accept,因此到达的请求将被保存在TCP层的队列中而不是在应用程序的队列中。如果持续过载,那么TCP层将最终发现它的请求队列被填满,因此同样会开始抛弃请求。当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。 |
线程的生命周期
线程生命周期的每一步如下所示:
- 1、新建:实例化线程对象
- 2、就绪:调用start将线程加入可调度线程池,等待CPU调度(分配时间片)。
- 3、运行:CPU从可调度线程池中分配时间片给线程,线程在未执行完毕情况下可能会在就绪和运行之间不断切换,程序员无法干预。
- 4、阻塞:线程有时会因为同步、锁、sleep等方式阻塞。
- 5、死亡:分为正常死亡(线程结束)和非正常死亡(线程终止)。
多线程的四种技术方案
多线程有四种技术方案分别是pthread、NSThread、GCD和NSOperation,如下图所示
多线程的几种方式
我们一般使用比较多的是GCD,因为开发者只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码,但这也是GCD的不够灵活的地方,我们无法监控线程的各个状态,这也是很多大框架中使用NSOperation的原因,NSOperation相比GCD更加灵活,开发者可以通过KVO监测Operation的状态,自定义NSOperation等。