文章搬运来源:https://juejin.cn/post/6924912442682114062#heading-17
作者:小峰子
对iOS开发感兴趣,可以看一下作者的iOS交流群:812157648,大家可以在里面吹水、交流相关方面的知识,群里还有我整理的有关于面试的一些资料,欢迎大家加群,大家一起开车
写在前面
多线程
是比较庞大和单独的模块,所以此多线程系列
计划从四个方面入手, 包括原理篇
,应用篇
,底层篇
,面试篇
。本文主要讲诉多线程相关的原理,后续文章依次讲诉其他。
1.进程和线程
1.1 什么是进程
进程
是指在系统中正在运行的一个可执行文件,每个进程
之间都是相互独立的,每个进程
运行在其专用且受保护的内存。
需要注意:
我们常说iOS
是单进程
的。这对也不对。
如果真是单进程
,完全没有后台,那想想推送是怎么被收到的,电话是怎么被唤醒的,就解释不通了。熟悉逆向
的同学,就知道有不少的后台在开机后就在运行了。
所以严格来说,应该是对于应用程序来说,iOS是单进程的。
至于为什么iOS采用单进程
?
因为多进程
间的切换会消耗大量资源,并且使用沙盒机制,保证系统更流畅安全的运行。
1.2 什么是线程
线程
是进程
的基本单元,进程
中的所有任务都在线程
中执行,进程
至少要有一条线程
才能执行任务。程序启动会默认开启一条线程
,称为主线程
。
从技术角度来看,线程
是管理代码执行所需的内核级和应用程序级数据结构的组合。内核级结构在一个可用内核上协调事件向线程的调度。应用程序级别的结构包括用于存储函数调用的调用堆栈,以及应用程序管理和操纵线程的属性和状态所需的结构。
简单来说,线程
依赖于内核级的调度来完成应用程序级的任务。
1.3 进程和线程的关系
地址空间:同一个进程
的线程
共享本进程
的地址空间,不同进程
的地址空间是互相独立的。
资源拥有:同一个进程
的线程
共享本进程
的资源,如内存,I/O,cpu等,不同进程
之间的资源是互相独立的。
异常处理:一个进程
崩溃后,不会对另一个进程
产生影响;一个线程
崩溃后,对应的进程
也会崩溃。
执行过程:每个独立的进程
都有应用程序入口,顺序执行序列;线程
不能独立执行,必须依赖于进程
。一个线程
中的任务是串行,同一时间,一个线程
只能执行一个任务,所以线程
是进程
的一条执行路径。
1.4 队列和线程的关系
队列
是一种满足先进先出
(FIFO)结构的运算受限的特殊线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。
队列
和线程
没有关系,却又总是一起出现。就像银行窗口不关心队伍是怎么排列的,只负责做业务。
队列
负责任务执行的顺序,线程
负责任务的具体执行
1.5 runloop和线程的关系
-
runloop
与线程
是一一对应的,一个runloop
对应一个核心线程 - 当
线程
的runloop
被开启后,线程
在执行完任务会进入休眠状态,任务来了会被唤醒执行 -
runLoop
在第一次被获取时创建,销毁则是在线程
结束的时候 - 主线程的
runLoop
在程序启动时默认创建好了,而子线程的runLoopd
需要我们主动创建和维护
2.多线程
2.1 多线程和单线程
单线程:
在非并行应用程序
中,只有一个执行线程
在执行任务。该线程以应用程序的main例程
开始和结束,并且一个一个地分支到不同的方法或函数,以实现应用程序的整体行为。
可以理解成一家银行
(进程),只开通一个窗口
(线程)在办理业务(执行任务),并且至少需要一个窗口
(线程)。
多线程:
在多线程
时,即支持并发的应用程序
从一个线程开始,并根据需要添加更多线程
以创建其他执行路径
。每个新路径都有其自己的自定义启动例程
,该例程
独立于应用程序main例程
中的代码运行。
可以理解成一家银行
(进程),开通多个窗口
(线程)在办理业务(执行任务)。
2.2 多线程的原理
对于单核设备,多线程
并发执行,其实是CPU
快速地在多条线程
之间调度,时间片在不停切换,如果CPU
调度线程
的时间足够快,就造成了多线程
并发执行的假象。对于多核设备,才是真正意义的多线程
。
这两种情况分别叫做多任务
和多处理
。
多任务
和多处理
是相关的概念,但却不相同。多任务
处理是一次处理多个不同任务的能力。多处理
是计算机同时使用多个处理器的能力。通常,如果在多任务
环境中有多个处理器可用,则可以在处理器之间分配任务。在这种情况下,任务可以同时运行。
如果线程
非常非常多,会发生什么情况?
CPU
会在N多线程
之间调度,消耗大量的CPU
资源,每条线程
被调度执行的频次会降低,导致程序效率降低。
2.3 多线程的利弊
多线程的利:
- 多线程可以提高应用程序的感知响应能力
- 多线程可以提高应用程序在多核系统上的实时性能
- 多线程可以适当提高资源的利用率
多线程的弊:
- 多线程会增加程序的复杂度,需要考虑线程之间的通信、多线程的数据共享等,出错可能性增加
- 如果开启大量的线程,会占用大量的内存空间
- 线程越多,CPU在调度线程上的开销就越大
2.4 线程的生命周期
新建
线程后,线程
以三种主要状态之一运行:运行
,就绪
或阻塞
。如果线程
当前未在运行,则要么阻塞并等待输入,要么准备运行。线程
在这些状态之间来回移动,直到最终退出并进入死亡
状态。
-
新建
:创建线程实例 -
就绪
:向线程发出star
消息,并把线程加入可调度线程池
-
运行
:被cpu
调度执行任务时 -
堵塞
:线程
被堵塞,如调用sleep
,同步锁,栅栏函数等 -
死亡
:任务执行完毕,并且没有新任务需要执行,或者线程
被强制退出
新建线程
时,必须为该线程
指定一个入口点函数。该入口点函数构成要在线程
上运行的代码。当函数返回时,或当强制退出
线程时,线程将永久停止并被系统回收。由于就内存和时间而言,创建线程
的成本相对较高,因此建议入口点函数进行大量工作或设置运行循环以允许重复执行工作,而非新建多条线程执行。
退出线程
的最佳方法自然是让线程到达其主入口点例程的末尾。尽管有强制退出线程
的功能,但这些功能仅应作为最后的手段使用。在线程到达其自然终点之前强制退出该线程
可能导致难以预料的后果。如果线程
已分配内存,打开文件或获取其他类型的资源,则可能无法回收这些资源,从而导致内存泄漏或其他潜在问题。
2.5 线程池原理
corePoolSize:核心线程数
maxPoolSize:最大线程数
复制代码
- 如果小于
corePoolSize
, 就创建线程
并执行该任务;否则,将该任务放入阻塞队列; - 如果能成功将任务放入阻塞队列中, 如果当前线程池是
非运行
状态,则将该任务从阻塞队列中移除,然后执行reject()
处理该任务;如果当前线程池处于运行
状态,则需要再次检查线程池(因为可能在上次检查后,有线程资源被释放),是否有空闲的线程;如果有则执行该任务; - 如果不能将任务放入阻塞队列中,说明阻塞队列已满;那么尝试创建一个新的
线程
去执行这个任务;如果执行失败,说明线程池中线程数达到maxPoolSize
,则执行reject()
处理任务;
线程池的饱和策略
:当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
-
AbortPolicy
:丢弃任务并抛出RejectedExecutionException
异常,阻止系统正常运行 -
CallerRunsPolicy
:将任务回退到调用者 -
DiscardOldestPolicy
:丢弃等待最久的任务,然后重新尝试执行任务(重复此过程) -
DisCardPolicy
:直接丢弃任务
当然也可以实现RejectedExecutionHandler
接口,自定义饱和策略,如任务日志记录。
2.6 多线程的技术方案
方案 | 简介 | 语言 | 生命周期 |
---|---|---|---|
pthread | 通用API,跨平台,使用难度大 | C | 程序员管理 |
NSThread | 面向对象,简单易用,可直接操作线程 | OC | 程序员管理 |
GCD | 充分利用设备的多核 | C | 自动管理 |
NSOperation | 面向对象,基于GCD,功能更多 | OC | 自动管理 |
基于手动管理线程生命周期很繁琐,而且容易出错,因此应尽可能避免使用pthread
和NSThread
。所以GCD
和NSOperation
的使用率更高。
2.7 任务执行的效率
影响任务执行的效率包括:
- 线程数和
cpu
的调度速度 - 任务的复杂度
- 任务的优先级
2.6 线程安全之库开发
不少同学日常会封装一些库
发布在同性交友网站,或提供团队使用。
封装过程中应注意,尽管可以控制应用程序
是否使用多线程执行,但是库
却不能。开发库
时,必须假定调用者是多线程
的,或者可以随时切换为多线程
。因此,应该始终对代码的关键部分使用锁
。
线程
编程的危害之一是多个线程之间的资源争用。如果多个线程
尝试同时使用或修改同一资源,则可能会出现问题。不能完全维护单独的资源时,必须使用锁
,条件
,原子操作
和其他技术来同步对资源的访问。
对于库
,仅当应用程序
成为多线程
时才创建锁
是不明智的。如果需要在某个时候锁定代码,请在使用库
的早期创建锁定对象,最好是通过某种显式调用来初始化库
。
广为流传的库
一定是线程安全的。
2.7 多线程的通讯
-
直接通讯
:直接在其他线程上执行选择器的功能,参考performSelectorOnMainThread
相关的API
-
全局变量
:尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁
或其他同步机制保护共享变量,以确保代码的正确性 -
条件锁
:等待条件的线程将保持阻塞状态,直到另一个线程显式通知该条件为止。参考NSCondition
-
runloop源
:比如设置timer
-
端口
:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术,它也是基于runloop
的,参考NSPort
-
消息队列
:消息队列是任务必须按先进先出顺序处理的数据(消息)的集合。参考NSNotificationQueue
写在后面
多线程是提高程序效率的开发利器,但它不应该被滥用产生难以定位的错误。合理使用多线程,人人有责。