目录
前言
学习此文:iOS多线程
在平时的iOS开发中,多线程是我们常会遇到的,开启新线程,比如pthread、NSThread、GCD、NSOperation,其中GCD、NSOperation是我们最常用。在研究这些之前,我们先来了解一些多线程方面的概念
iOS开发是单进程,Android可以是多进程
进程和线程
什么是进程
- 进程是指在系统中正在运⾏的⼀个应⽤程序,它是程序执行时的一个实例
- 程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行
- 每个进程之间是独立的,每个进程运行在其专有的且受保护的内存空间内
- 在 Mac电脑上,可以通过"活动监视器"查看所开启的进程
. . . .
什么是线程
- 线程是进程的基本执行单元,一个进程中的所有任务都是在线程中执行的
- 进程想要执行任务必须得有线程,一个进程至少有一条线程
- 程序启动是会默认开启一条线程,这条线程被称为主线程或者UI线程
进程和线程的区别
- 进程是资源分配的最小单位,线程是程序执行的最小单位
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换或创建一个线程的花销远比进程要小很多
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点
- 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间
多线程的意义
objectivec
NSLog(@"开始");
NSInteger count = 1000 * 100;
for (NSInteger i = 0; i < count; i++) {
// 栈区
NSInteger num = i;
// 常量区
NSString *name = @"RENO";
// 堆区
NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
NSLog(@"%@", myName);
}
NSLog(@"结束");
上面的代码中,执行了10万次循环,每次循环都会创建局部变量,此过程执行完成耗时10秒,如果此流程放在主线程,会造成主线程卡顿,极大影响用户体验
所以通常情况下,我们都会进行异步处理,开启新的线程对这些事务进行处理,而如果一个事务很复杂,比较耗时,可以将一个大的事务拆分成多个小的事务进行并发处理,这样可以节省时间,并且不会影响用户的体验
多线程的优缺点
相较单线程
优点:
- 提高程序的执行效率
- 提高资源的利用率(如CPU、内存)
- 线程上人物执行完毕后,线程会自动销毁
缺点:
- 开启线程需要占⽤⼀定的内存空间(默认情况下,iOS主线程占用1M,子线程占512KB),如果开启⼤量的线程,CPU在调⽤线程上的开销就越⼤,会占⽤⼤量的内存空间,降低程序的性能
- 程序设计更加复杂,⽐如线程间的通信、多线程的数据共享
时间片概念
开启过多的线程也会导致性能的下降,这里涉及到时间片的概念。多线程的执行是CPU快速的在多个线程之间进行切换。线程数过多,CPU会在多个线程之间切换,创建和销毁大量的CPU资源,反而导致执行效率的下降
- 时间⽚的概念:CPU在多个任务直接进⾏快速的切换,这个切换的时间间隔就是时间⽚。(单核CPU)同⼀时间,CPU只能处理1个线程,换⾔之,同⼀时间只有 1 个线程在执⾏
- 多线程同时执⾏:是CPU快速的在多个线程之间的切换,CPU调度线程的时间⾜够快,就造成了多线程的同时执⾏的效果
- 如果线程数⾮常多,CPU会在N个线程之间切换,消耗⼤量的CPU资源,每个线程被调度的次数会降低,线程的执⾏效率降低
理解:
多线程程序可以在多个线程之间反复多次进行上下文切换,宏观上看上去就好像单个CPU核能够并列执行多个线程一样。而且在多核CPU的情况下,就是真的在并行执行多个线程
线程的生命周期
App有生命周期,那么线程的生命周期是什么样子的呢?
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。见下图:
- 新建:通过创建线程的函数,创建一个新线程
- 就绪:调用线程的start方法,这时线程处于等待CPU分配资源阶段,谁先抢占CPU资源,谁开始执行
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
- 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep、等待同步锁,线程就从可调度线程池移出,处于了阻塞状态,这个时候sleep到时、获取同步锁,此时会重新添加到可调度线程池。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态
- 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源
线程池的运行策略
运行策略
队列满且正在运行的线程数量小于最大线程数,则新进入的任务,会直接创建非核心线程工作
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们
- 当有任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize(核心线程数),那么马上创建核心线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果队列满了,而且正在运行的线程数量小于maximumPoolSize(最大线程数),那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池饱和策略将进行处理
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做,超过一定的时间(超时)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小
饱和策略
如果线程池中的队列满了,并且正在运行的线程数量已经大于等于当前线程池的最大线程数,则进行饱和策略的处理
- AbortPolicy直接抛出RejectedExecutionExeception异常来阻⽌系统正常运⾏
- CallerRunsPolicy将任务回退到调⽤者
- DisOldestPolicy丢掉等待最久的任务
- DisCardPolicy直接丢弃任务
自旋锁和互斥锁
锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用
注:不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了
自旋锁
使用一种用于用于保护多线程共享资源的锁,当自旋锁尝试获取锁时以忙等待(busy waiting,线程在这一过程中保持执行)的形式不断地循环检查锁是否可用
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行
自旋锁:OSSpinLock、dispatch_semaphore_t
互斥锁
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪,Runnable)
互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock、NSCondition、NSRecursiveLock
自旋锁和互斥锁区别
- 自旋锁会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源显式释放锁
- 互斥锁会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时CPU可以调度其他线程工作,直到 被锁资源 释放锁。此时会唤醒休眠线程
- 优缺点
- 优点:因为自旋锁不会引起调用者睡眠,所以不会进行线程调度、CPU时间片轮转等耗时操作。所以如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁
- 缺点:自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用
原子属性
- OC在定义属性时有nonatomic和atomic两种选择,默认为atomic属性
- atomic:原子属性,底层是通过Spinlock为setter方法加自旋锁(即为单写set多读get)
- nonatomic:非原子属性,不会为setter方法加锁
- nonatomic和atomic的对比
- atomic:线程安全,需要消耗大量的资源
- nonatomic:非线程安全,适合内存小的移动设备
- iOS开发建议
- 如非需抢占资源的属性(如购票,充值),所有属性都声明为nonatomic
- 尽量避免多线程抢夺同一块资源
- 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
- atomic底层原理
属性setter方法底层调用的是objc_setProperty函数,其会调用reallySetProperty函数,该函数的实现中,针对原子属性,添加了spinlock锁:
Spinlock是Linux内核中提供的一种比较常见的锁机制,自旋锁是原地等待的方式解决资源冲突的,即一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地打转(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 ------ 自旋锁不应该被长时间的持有(消耗CPU资源)