本篇提纲:
1、线程与进程
2、多线程
3、多线程相关面试题
4、线程安全问题
5、线程与runloop的关系
线程与进程
-
进程
进程
是指可以在操作系统中独立运行并且作为资源分配的基本单位;
例如正在运行的一个应用程序,我们可以理解为一个进程
;
进程
和进程
之间是相互独立的,每个进程
都运行在其专用的且受保护的内存空间; -
线程
线程
是操作系统中作为调度和分派的基本单位;
线程
是进程的基本执行单元,一个进程的所有任务都在线程中执行;
程序启动会默认开启一条线程
,这条线程我们称为主线程
或者UI线程
; -
进程与线程的关系
1、地址空间:同一个进程的线程共享本进程的地址空间,而多进程的地址空间是相互独立的。
2、资源拥有:同一进程的线程共享本进程的资源,如:内存、IO、CPU等,而多进程之间的资源是独立的。
他们之间的关系可以用不同的公司和生产线来比喻,比如我们现在有很多正在运作的公司,每个公司我们可以理解为一个进程;而每个公司的内部有很多部门,比如产品部、市场部、技术部去生产不同的东西,可以理解为公司内部的三条线程,各自有各自的任务,但是整个公司的空间和资源是这三个线程可以共享的。
多进程要比多线程更加健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃了会导致整个进程都崩溃掉。
我看到了一篇解释进程和线程关系的比较清楚易懂的文章,还有疑问可以看下进程和线程关系的简单解释
多线程
多线程
的实现原理:
iOS中的多线程,由CPU在多个任务之间进行快速切换,CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。
时间片:CPU在多个任务之间进行快速的切换,这个时间间隔称为时间片
。
如果开启的线程太多会导致的问题:
1、CPU会在N个线程之间来回切换,会消耗大量的CPU资源。
2、每个线程的调度次数会被降低,线程的执行效率会很低。
多线程的好处:
1、能适当提高程序的执行效率。
2、能适当的提高CPU、内存等资源的利用率。
多线程会带来的问题:
1、开启线程需要占用内存,默认情况下,每一个线程都占512KB。
2、会让程序设计更加复杂,比如线程之间的通信,多线程的资源共享问题。
iOS多线程技术方案:
方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | - 一套通用的多线程API - 适用于Unix/Linux/Windows等系统 - 跨平台、可移植 - 使用难度大 |
C语言 | 程序员管理 | 几乎不用 |
NSThread | - 面向对象 - 简单易用,可直接操作线程对象 |
OC语言 | 程序员管理 | 偶尔使用 |
GCD | - 旨在替代NSThread等线程技术 - 充分利用设备多核 |
C语言 | 自动管理 | 经常使用 |
NSOperation | - 基于GCD(底层是GCD) - 比GCD多了一些更简单实用的功能 -使用更加面向对象 |
OC语言 | 自动管理 | 经常使用 |
多线程生命周期:
线程的生命周期主要分为五个阶段:
-
新建
实例化线程对象 -
就绪
线程对象调用start方法,将线程对象加入到可调度的线程池,等待系统分配CPU进行调度。调用start方法并不会立即执行,而是进入到就绪
状态,之后CPU调度该线程,才会进入运行状态。 -
运行
CPU负责调度可调度线程池中线程的执行,直到因为等待资源被阻塞或者执行完任务而死亡,在运行期间,其状态可能会在就绪和运行之间进行切换,这个变化由CPU负责,分配相应的时间片给线程。 -
阻塞
处于运行状态的某些线程,在某种情况下,比如执行了sleep,或者等待I/O资源等情况,线程将让出CPU暂停自己的运行,进入阻塞状态。
当阻塞的原因消除时,被阻塞的线程会重新回到就绪队列,回到就绪状态。 -
死亡
死亡大致分为三种情况:
1、线程执行任务完毕,正常死亡;
2、线程被强制终止,比如通过stop方法终止线程;
3、线程抛出未捕获的异常;
线程池原理:
1、先去判断核心线程池是否都在执行任务
- 返回NO,创建新的任务,让线程去执行
- 返回YES,进入2
2、判断线程池工作队列是否饱满
- 返回NO,将任务存储到工作队列,等待CPU调度
- 返回YES,进入3
3、判断线程池中的线程是否都处于执行状态
- 返回NO,安排可调度线程池中的空闲线程去执行
- 返回YES,进入4
4、交给饱和策略去执行,分为以下四种拒绝策略:
- AbortPolicy:直接抛出RejectedExecutionExeception异常,阻止系统正常运行
- CallerRunsPolicy:将任务回退到调用者
- DisOldestPolicy:丢掉等待最久的任务
- DisCardPolicy:直接丢掉任务
四种拒绝策略均实现的RejectedExecutionHandler接口
多线程相关面试题
-
任务执行速度的影响因素
1、CPU的调度情况
2、任务的复杂度
3、任务的优先级
4、线程的状态 -
优先级调度
线程的优先级不仅可以由用户手动设置,系统还会根据不同的线程的表现自动调整优先级。通常情况下频繁的进入等待状态的线程比频繁进行大量计算,以至于每次都要把时间片全部用尽的线程要受欢迎。频繁等待的线程通常占用很少的时间。 -
IO密集型线程(IO Bound Thread):频繁等待的线程
-
CPU密集型线程(CPU Bound Thread):很少等待的线程。
IO密集型比CPU密集型更容易得到线程优先级的提升
IO操作的速度很慢,并且需要频繁的等待,如果它的优先级又低很容易被饱和策略所淘汰。为了避免这种情况,当CPU发现一个频繁等待的线程,会提升它的优先级,从而提升线程被执行的可能性。
- 优先级的影响因素
1、用户指定优先级。例如QualityOfService
2、 根据进入等待状态的频繁程度提升或降低优先级。
3、长时间得不到执行而被提升优先级。
线程安全问题
- 互斥锁
同一时间只有一条线程能执行,当发现有其他线程正在执行,当前线程会进入休眠状态,等待被唤醒。
使用注意事项:
互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差。
能够加锁的任意NSObject对象
锁对象一定要保证所有的线程都能够访问
- 自旋锁
自旋锁与互斥锁类似,但是它不是让线程处于休眠状态,而是在没有获取锁之前不放弃当前的时间片,一直处于一个忙等的状态,一直询问“到我没,到我没”这样一个状态。
自旋锁的使用场景:锁持有的时间短,或者线程不希望在重新调度上花太多成本时,就可以使用自旋锁。
使用自旋锁,当新的线程访问代码时,如果发现有其他线程已经锁定了代码,新线程会用一种死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能。
附一张网上查到的自旋锁和非自旋锁的流程图:
自旋锁和非自旋.png
自旋锁的好处:
自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
因为阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能切换线程带来的开销比实际业务代码执行的开销还要大。
在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。
- atomic与nonatomic
atomic
:原子属性,针对多线程设计的,默认值。线程安全,需要消耗大量的资源。
nonatomic
:非原子属性,非线程安全,没有锁,性能高,移动端常用。
关于atomic的源码分析,来看下内部的锁:
image.png
它的内部实现是添加的这种spinlock_t
锁,而spinlock_t
是:
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
public:
.......
}
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
根据注释部分的描述,可以了解到os_unfair_lock
加完锁之后,会让另外一个线程进入休眠状态,而不是忙等,所以其实是互斥锁,这个改变是在iOS10之后,在iOS10之前使用的是OSSpinLock
,之所以被os_unfair_lock
替代也是出于线程饿死的问题。
线程与RunLoop的关系
1、RunLoop与线程是一一对应的,一个Runloop对应一个核心线程。为什么说是核心的,因为RunLoop里可以进行嵌套,但是核心只能有一个,他们的关系保存在一个全局字典里。
2、RunLoop是来管理线程的,当线程的RunLoop被开启后,线程会在执行完任务进入休眠状态,有了任务就会唤醒去执行任务。
3、RunLoop在第一次获取时被创建,在线程结束时被销毁。
4、主线程的RunLoop,在程序启动的时候默认创建。
5、对于子线程来说,RunLoop是懒加载的,只有当我们使用的时候才会去创建。所以,当子线程使用定时器时,要确保子线程的RunLoop被创建,不然定时器无法进行回调。
网友评论