在平时的iOS开发中,多线程是我们常会遇到的,开启新线程,比如pthread、NSThread、GCD、NSOperation,其中GCD、NSOperation是我们最常用。在研究这些之前,我们先来了解一些多线程方面的概念。
1.线程和进程
1.定义
- 线程的定义:
- 线程是进程的基本执⾏单元,⼀个进程的所有任务都在线程中执⾏
- 进程要想执⾏任务,必须得有线程,进程⾄少要有⼀条线程
- 程序启动会默认开启⼀条线程,这条线程被称为主线程或
UI线程
- 进程的定义:
- 进程是指在系统中正在运⾏的⼀个应⽤程序
- 每个进程之间是独⽴的,每个进程均运⾏在其专⽤的且受保护的内存空间内
- 通过
活动监视器可以查看Mac系统中所开启的进程
如上图Mac活动监视器中,罗列出了当前运行的进程,各个进程之间相互独立运行,每个进程内会有多个线在运行。
2.进程与线程的关系
-
地址空间:同⼀进程的线程共享本进程的地址空间,⽽进程之间则是独⽴的地址空间。 -
资源拥有:同⼀进程内的线程共享本进程的资源,如内存、I/O、cpu等,但是进程之间的资源是独⽴的。 -
进程和线程的关系:
- ⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进程都死掉。所以多进程要⽐多线程健壮。
- 进程切换时,消耗的资源⼤,效率⾼。所以涉及到频繁的切换时,使⽤线程要好于进程。同样如果要求同时进⾏并且⼜要共享某些变量的并发操作,只能⽤线程不能⽤进程。
- 执⾏过程:每个独⽴的进程有⼀个程序运⾏的⼊⼝、顺序执⾏序列和程序⼊⼝。但是线程不能独⽴执⾏,必须依存在应⽤程序中,由应⽤程序提供多个线程执⾏控制。
- 线程是处理器调度的基本单位,但是进程不是。
- 线程没有地址空间,线程包含在进程地址空间中。
3.多线程的意义
在iOS开发中,我们更多的是进行多线程的开发,那么多线程开发的意义有哪些呢?引入一个案例,见下图:
在上面的案例中,执行千万次的循环,分别在主线程 和 dispatch_async 块中调用。主线程中耗时 14秒(阻塞主线程14秒)放到dispatch_async 块中则不会阻塞主线程。
多线程的优缺点:
-
优点
- 能适当提⾼程序的执⾏效率
- 能适当提⾼资源的利⽤率(如
CPU,内存) - 线程上的任务执⾏完成后,线程会⾃动销毁
-
缺点
- 开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占
512KB) - 如果开启⼤量的线程,会占⽤⼤量的内存空间,降低程序的性能
- 线程越多,
CPU在调⽤线程上的开销就越⼤ - 程序设计更加复杂,⽐如线程间的通信、多线程的数据共享
- 开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占
4. 时间片概念
开启过多的线程也会导致性能的下降,这里涉及到时间片的概念。多线程的执行是CPU快速的在多个线程之间进行切换。线程数过多,CPU会在多个线程之间切换,消耗大量的CPU资源,反而导致执行效率的下降。
时间⽚的概念:CPU在多个任务直接进⾏快速的切换,这个时间间隔就是时间⽚。(单核CPU)同⼀时间,CPU只能处理1个线程,换⾔之,同⼀时间只有 1 个线程在执⾏多线程同时执⾏:是CPU快速的在多个线程之间的切换,CPU调度线程的时间⾜够快,就造成了多线程的同时执⾏的效果如果线程数⾮常多,CPU会在N个线程之间切换,消耗⼤量的CPU资源,每个线程被调度的次数会降低,线程的执⾏效率降低
2.线程的生命周期
线程的生命周期包含5个阶段,包括:创建、就绪、运行、阻塞、销毁。见下图:
-
创建:就是刚通过alloc,创建出来的线程; -
就绪:就是调用的线程的start方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行; -
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能; -
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep、等待同步锁,线程就从可调度线程池移出,处于了阻塞状态,这个时候sleep到时、获取同步锁,此时会重新添加到可调度线程池。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态; -
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。
3.线程池的运行策略
-
线程池运行策略
线程池的运行策略,见下图:
-
队列满且正在运行的线程数量小于最大线程数,则新进入的任务,会直接创建非核心线程工作。
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
-
当有任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于
corePoolSize(核心线程数),那么马上创建核心线程运行这个任务; - 如果正在运行的线程数量大于或等于
corePoolSize,那么将这个任务放入队列; - 如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize(最大线程数),那么还是要创建非核心线程立刻运行这个任务; - 如果队列满了,而且正在运行的线程数量大于或等于
maximumPoolSize,那么线程池饱和策略将进行处理。
- 如果正在运行的线程数量小于
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做,超过一定的时间(
超时)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
-
- 饱和策略
如果线程池中的队列满了,并且正在运行的线程数量已经大于等于当前线程池的最大线程数,则进行饱和策略的处理。
AbortPolicy直接抛出RejectedExecutionExeception异常来阻⽌系统正常运⾏CallerRunsPolicy将任务回退到调⽤者DisOldestPolicy丢掉等待最久的任务DisCardPolicy直接丢弃任务
4.自旋锁和互斥锁
1. 自旋锁
是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。
iOS中自旋锁:OSSpinLock、dispatch_semaphore_t
2. 互斥锁
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪)。
iOS 互斥锁:pthread_mutex、@synchronized、NSLock、NSConditionLock、NSCondition、NSRecursiveLock
3. 自旋锁和互斥锁的特点
-
自旋锁会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。 -
互斥锁会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。 -
自旋锁优缺点
优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。
4.原子属性和非原子属性
-
OC在定义属性时有nonatomic和atomic两种选择,默认为atomic属性atomic:原子属性,为setter方法加自旋锁(即为单写多读)nonatomic:非原子属性,不会为setter方法加锁
-
nonatomic和atomic的对比atomic:线程安全,需要消耗大量的资源;nonatomic:非线程安全,适合内存小的移动设备。
-
iOS开发的建议- 如非需抢占资源的属性(如购票,充值),所有属性都声明为
nonatomic。 - 尽量避免多线程抢夺同一块资源。
- 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。
- 如非需抢占资源的属性(如购票,充值),所有属性都声明为
-
原理探索
我们在探索类的本质时,对于类的属性的
setter方法,系统会有一层objc_setProperty的封装,如下图:
Spinlock是Linux内核中提供的一种比较常见的锁机制,自旋锁是原地等待的方式解决资源冲突的,即:一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地打转(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗CPU资源)。
注意:atomic只是原子属性、一个标识符,本身并不是自旋锁,而是底层通过Spinlock实现自旋锁。
5. iOS技术方案
iOS技术方案,见下图: