一、线程与进程① 线程与进程的定义线程线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;进程要想执行任务,必须得有线程,进程至少要有一条线程;程序启动会默认开启一条线程,这条线程被称为主线...

一、线程与进程
① 线程与进程的定义
- 线程
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;
- 进程要想执行任务,必须得有线程,进程至少要有一条线程;
- 程序启动会默认开启一条线程,这条线程被称为主线程或者 UI 线程。
- 进程
- 进程是指在系统中正在运行的一个应用程序;
- 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内;
- 通过“活动监视器”可以查看 mac 系统中所开启的线程。
② 线程与进程的关系
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
- 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、 cpu等,但是进程之间的资源是独立的;
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮;
- 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程;
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
- 线程是处理器调度的基本单位,但是进程不是;
- 线程没有地址空间,线程包含在进程地址空间中。
③ 线程与Runloop的关系
- runloop 与线程是一一对应的,一个 runloop 对应一个核心的线程,为什么说是核心的,是因为 runloop 是可以嵌套的,但是核心的只能有一个,它们的关系保存在一个全局的字典里;
- runloop 是来管理线程的,当线程的 runloop 被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务;
- runloop 在第一次获取时被创建,在线程结束时被销毁;
- 对于主线程来说,runloop 在程序一启动就默认创建好了;
- 对于子线程来说,runloop 是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的 runloop 被创建,不然定时器不会回调。
二、多线程概念
① 多线程的原理
- 对于单核 CPU,同一时间,CPU 只能处理一条线程,换言之,同一时间段内只有一条线程在执行;
- iOS 中的多线程同时执行的本质是 CPU 在多个任务直接进行快速的切换,由于 CPU 调度线程的时间足够快,就造成了多线程的“同时”执行的效果;
- 如需线程数非常多,CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源;每个线程被调度的次数会降低,线程的执行效率会降低;
- 多线程是一个比较轻量级的方法,来实现单个应用程序内多个代码执行路径;
- 在系统级别内,程序并排执行,程序分配到每个程序的执行时间是基于该程序的所需时间和其他程序的所需时间来决定的;
- 然而,在每个程序内部,存在一个或者多个执行线程,它同时或在一个几乎同时发生的方式里执行不同的任务。
② 多线程的意义
- 多线程的优势:
- 能适当提高程序的执行效率;
- 能适当提高资源的利用率,如CPU、内存;
- 线程上的任务执行完成后,线程会自动销毁;
- 多线程的劣势:
- 开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB;
- 如果开启大量线程,会占用大量的内存空间,降低程序的性能;
- 线程越多,CPU 在调用线程上的开销就越大;
- 程序设计更加复杂,比如线程间的通信,多线程的数据共享。
③ 多线程的生命周期
- 就绪:线程对象调用 start 方法,将线程对象加入可调度线程池,等待 CPU 的调用,即调用 start 方法,并不会立即执行,而是进入就绪状态,需要等待一段时间,经 CPU 调度后才执行,也就是从就绪状态进入运行状态;
- 运行:CPU 负责调度可调度线城市中线程的执行,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由 CPU 负责,开发人员无法干预;
- 阻塞:当满足某个预定条件时,可以使用休眠,即 sleep,或者同步锁,阻塞线程执行。当进入sleep时,会重新将线程加入就绪中;以 NSThread 为例,进行休眠时间设置:
- sleepUntilDate: 阻塞当前线程,直到指定的时间为止,即休眠到指定时间;
- sleepForTimeInterval: 在给定的时间间隔内休眠线程,即指定休眠时长;
- 同步锁:@synchronized(self);
- 死亡:分为两种情况:
- 正常死亡,即线程执行完毕;
- 非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出);
- 处于运行中的线程拥有一段可以执行的时间,即时间片(CPU在多个任务直接进行快速切换的时间间隔称为时间片):
- 如果时间片用尽,线程就会进入就绪状态队列;
- 如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列;
- 等待事件发生后,线程又会重新进入就绪状态队列;
- 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列中选择一个线程继续执行;
④ 线程池
- 饱和策略
- AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行;
- CallerRunsPolicy:将任务回退到调用者;
- DisOldestPolicy:丢掉等待最久的任务;
- DisCardPolicy:直接丢弃任务。
三、多线程实现
方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的线程API,适用于Unix/Linux/Window等系,跨平台,可移植,使用难度大 | C | 程序员管理 | 几乎不用 |
NSThread | 使用更加面向对象,可直接操作线程对象 | OC | 程序员管理 | 偶尔使用 |
GCD | 旨在代替NSThread等线程技术,充分利用设备的多核 | C | 自动管理 | 经常使用 |
NSOperation | 基于GCD,比GCD多了部分更加简单实用的工能,使用更加面向对象 | OC | 自动管理 | 经常使用 |
① pthread
- POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。
- 在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。
- 简单地说,这是一套在很多操作系统上都通用的多线程API,所以移植性很强。
- 需要导入#import <pthread.h>,使用如下:
- 看代码就会发现它需要 c 语言函数,这是比较难收的,更难受的是还需要手动处理线程的各个状态的转换即管理生命周期,比如,这段代码虽然创建了一个线程,但并没有销毁。
② NSThread
- NSThread 是经过苹果封装后的,并且完全面向对象的。所以可以直接操控线程对象,非常直观和方便。
- 但是,它的生命周期还是需要手动管理,所以这套方案也是偶尔用用,比如 [NSThread currentThread],它可以获取当前线程类,你就可以知道当前线程的各种属性,用于调试十分方便。
- 先创建线程类,再启动:
- 创建并自动启动:
- 使用 NSObject 的方法创建并自动启动:
- NSThread 的其他方法:
③ GCD
- Grand Central Dispatch,它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要手动管理,我们只需要告诉它执行什么就行。
- 同时它使用的也是 c 语言,不过由于使用了 Block(Swift里叫做闭包),使得使用起来更加方便,而且灵活。
- GCD 的使用,我将会单独整理一篇博客进行详细的说明,后续将会更新。
④ NSOperation
- 虽然 GCD 的功能已经很强大,但是它使用的 API 依然是 C 语言的。在某些时候,在面向对象的objective-c中使用起来非常的不方便和不安全。
- 所以苹果把 GCD 中的操作抽象成 NSOperation 对象,把队列抽象成 NSOperationQueue 对象。
- NSOperation 特点:
- 可以控制暂停、恢复、停止:suspended、cancel、cancelAllOperations;
- 可以控制任务的优先级:threadPriority 和 queuePriority;
- 可以设置依赖关系:addDependency 和 removeDependency;
- 可以控制并发个数:maxConcurrentOperationCount;
- NSOperation 有两个封装的便利子类 NSBlockOperation、NSInvocationOperation, 它们都使用了并发队列。
- NSOperation 只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务,分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会默认在当前队列同步执行。当然也可以在中途取消一个任务,只需要调用其 cancel 方法即可。
- NSInvocationOperation 使用如下,需要传入一个方法名:
- NSBlockOperation 使用如下:
- 这样的任务,默认会在当前线程执行。但是 NSBlockOperation 还有一个方法:addExecutionBlock: ,通过这个方法可以给 Operation 添加多个执行 Block。这样 Operation 中的任务会并发执行,它会在主线程和其它的多个线程执行这些任务,如下:
- 打印输出如下:
- addExecutionBlock 方法必须在 start() 方法之前执行,否则就会报错:
- 除了上面的两种 Operation 以外,还可以自定义 Operation。自定义 Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑。
- 到此为止,我们可以调用一个 NSOperation 对象的 start() 方法来启动这个任务,但是这样它会默认是同步执行的,就算是 addExecutionBlock 方法,也会在当前线程和其他线程中执行,也就是说还是会占用当前线程,这是就要用到队列 NSOperationQueue 了。并且,按类型来说的话一共有两种类型:主队列、其他队列,只要添加到队列,会自动调用任务的 start() 方法。
- 主队列:
- 其他队列的任务会在其他线程并行执行:
⑤ C和OC的桥接
- __bridge只做类型转换,但是不修改对象(内存)管理权;
- __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象;
- __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。
四、线程安全问题
当多个线程同时访问一块资源时,容易引发数据错乱和数据安全问题,这个时候就需要互斥锁(即同步锁)和自旋锁来解决了。
① 互斥锁
- 作用与意义:
- 用于保护临界区,确保同一时间,只有一条线程能够执行;
- 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象;
- 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。
- 注意:
- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差;
- 能够加锁的任意 NSObject 对象;
- 锁对象一定要保证所有的线程都能够访问。
② 自旋锁
- 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态;
- 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符 atomic,本身就有一把自旋锁;
- 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能。
③ 互斥锁和自旋锁对比
- 相同点:在同一时间,保证了只有一条线程执行任务,即保证了相应同步的功能;
- 不同点:
- 互斥锁:发现其他线程执行,当前线程 休眠(即就绪状态),进入等待执行,即挂起。一直等其他线程打开之后,然后唤醒执行;
- 自旋锁:发现其他线程执行,当前线程 一直询问(即一直访问),处于忙等状态,耗费的性能比较高;
- 场景:根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理:
- 当前的任务状态比较短小精悍时,用自旋锁;
- 反之则用互斥锁。
④ atomic 原子锁 & nonatomic 非原子锁
- atomic是原子属性,是为多线程开发准备的,默认属性。
- 仅仅在属性的 setter 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行写操作;
- 同一时间 单(线程)写多(线程)读的线程处理技术;
- Mac开发中常用;
- nonatomic 是非原子属性:
- 没有锁,性能高;
- 移动端开发常用;
五、线程与队列
- 队列是保存以及管理任务的,将任务加到队列中,任务会按照加入到队列中先后顺序依次执行。
- 如果是全局队列和并行队列,则系统会根据系统资源去创建新的线程去处理队列中的任务,线程的创建、维护和销毁由操作系统管理,还有队列本身是线程安全的。
- 使用 NSOperationQueue 实现多线程的时候是可以控制线程总数及线程依赖关系的,而 GCD 只能选择并行或者串行队列。
① 资源竞争
- 多线程同时执行任务能提高程序的执行效率和响应时间,但是多线程不可避免地遇到同时操作同一资源的情况。例如,如下一个资源竞争的问题,该怎么解决呢?
- 解决办法:
- @property (nonatomic, strong) NSString *target;将nonatomic改成atomic;
- 将并行队列 DISPATCH_QUEUE_CONCURRENT 改成串行队;
- DISPATCH_QUEUE_SERIAL;
- 异步执行 dispatch_async 改成同步执行 dispatch_sync;
- 赋值使用 @synchronized 或者上锁。
② 死锁
任何事情都有两面性,就像多线程能提升效率的同时,也会造成资源竞争的问题。而锁在保证多线程的数据安全的同时,粗心大意之下也容易发生问题,那就是死锁。
NSOperationQueue
- 鉴于 NSOperationQueue 高度封装,使用起来非常简单,一般不会出现什么问题。如下,案例展示了一个不好示范,通常我们通过控制 NSOperation 之间的从属关系,来达到有序执行任务的效果,但是如果互相从属或者循环从属都会造成所有任务无法开始。
- 解决办法:
GCD
- 在主线程同步执行造成 EXC_BAD_INSTRUCEION 错误:
- 和主线程同步执行类似,在串行队列中嵌套使用同步执行任务,同步队列 task1 执行完成后才能执行 task2 ,而 task1 中嵌套了task2 导致 task1 注定无法完成。
- 嵌套同步执行任务确实很容易出 bug ,但不是绝对,将同步队列DISPATCH_QUEUE_SERIAL 换成并行队列 DISPATCH_QUEUE_CONCURRENT 这个问题就迎刃而解。修改成并行队列后案例中 task1 仍然要先执行完嵌套在其中的 task2 ,而 task2 开始执行时,队列会另起一个线程执行 task2 , task2 执行完成后 task1 继续执行。
- 在很多人印象中,异步执行不容易发生互相等待的情况,确实,即使是串行队列,异步任务会等待当前任务执行后再开始:
- 常规死锁,在已经上锁的情况下再次上锁,形成彼此等待的局面:
- 要解决也比较简单,将 NSLock 换成递归锁 NSRecursiveLock,递归锁就像普通的门锁,顺时针转一圈加锁后,逆时针一圈即解锁;而如果顺时针两圈,同样逆时针两圈即可解锁。