基本概念
计算机操作系统都有的基本概念,以下概念简单方式来描述。
进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动。可以理解成一个运行中的应用程序。
线程:程序执行流的最小单元,线程是进程中的一个实体。
同步:只能在当前线程按先后顺序依次执行,不开启新线程。
异步:可以在当前线程开启多个新线程执行,可不按顺序执行。
队列:装载线程任务的队形结构。
并发:线程执行可以同时一起进行执行。
串行:线程执行只能依次逐一先后有序的执行。
进程和线程的比较
- 线程是CPU调用(执行任务)的最小单位。
- 进程是CPU分配资源的最小单位。
- 一个进程中至少要有一个线程。
- 同一个进程内的线程共享进程的资源。
一个进程可有多个线程,一个进程可有多个队列,队列可分并发队列和串行队列。
另外说到多线程就说说并发和并行,在知乎上看到一个人的回答感觉很形象:
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
它们的关键点就是:是否是同时
多线程原理
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行),多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
那么如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,同时每条线程被调度执行的频次也会会降低(线程的执行效率降低)。
因此我们一般只开3-5条线程。
多线程优缺点
多线程的优点
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)
多线程的缺点
创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间
如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。
程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题。
多线程的应用
主线程的主要作用
显示/刷新UI界面
处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意
别将比较耗时的操作放到主线程中
耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
将耗时操作放在子线程中执行,提高程序的执行效率
iOS多线程几种实现方式
iOS 中实现多线程的方法有4种
- Pthreads
- NSThread
- GCD
- NSOperation
其中pthread,GCD是基于C的。NSThread,NSOperationQueue基于OC的。
iOS多线程的对比
由于开发中都是用NSThread,GCD,NSOperation。所以此处只比较这三种
NSThread
每个NSThread对象对应一个线程,真正最原始的线程。
优点:NSThread 轻量级最低,相对简单。
缺点:手动管理所有的线程活动,如生命周期、线程同步、睡眠等。
NSOperation
自带线程管理的抽象类。
优点:自带线程周期管理,操作上可更注重自己逻辑。
缺点:面向对象的抽象类,只能实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。
GCD
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。
优点:最高效,避开并发陷阱。
缺点:基于C实现。
多线程的实现
由于pthread几乎不使用,这里不做讨论
一、NSThread的实现
1.创建线程
1 | // 方法一:创建线程,需要自己开启线程 |
后面两种方法都不用我们开启线程,相对方便快捷,但是没有办法拿到子线程对象,没有办法对子线程进行更详细的设置,例如线程名字和优先级等。
2.NSThread的属性
1 | // 获取当前线程 |
3.NSThread线程的状态
1 | 启动线程 |
4.NSThread多线程安全隐患
多线程安全隐患的原因:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。
那么当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
针对NSThread多线程安全隐患。我们可以使用互斥锁1
2
3@synchronized(锁对象) {
// 需要锁定的代码
}
互斥锁的使用前提:多条线程抢夺同一块资源时
注意:锁定1份代码只用1把锁,用多把锁是无效的
互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
5.NSThread线程之间的通信
什么叫做线程间通信
在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信,例如我们在子线程完成下载图片后,回到主线程刷新UI显示图片
线程间通信的体现
1个线程传递数据给另1个线程
在1个线程中执行完特定任务后,转到另1个线程继续执行任务
线程间通信常用的方法1
2
3
4// 返回主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
二、NSOperation的实现
NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能,所以使用起来更加方便易于理解。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。
NSOperation和NSOperationQueue实现多线程的具体步骤
- 将需要执行的操作封装到一个NSOperation对象中
- 将NSOperation对象添加到NSOperationQueue中
系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行
1.NSOperation的创建
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
使用NSOperation子类的方式有3种
NSInvocationOperation
1 | /* |
NSBlockOperation(最常用)
1 | //1.封装操作 |
自定义子类继承NSOperation,实现内部相应的方法
1 | // 重写自定义类的main方法实现封装操作 |
自定义类封装性高,复用性高。
2.NSOperationQueue的使用
主队列:通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行
非主队列:直接alloc init出来的队列。非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行
NSOperationQueue的作用
NSOperation可以调用start方法来执行任务,但默认是同步执行的
如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
添加操作到NSOperationQueue中1
2- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
注意:将操作添加到NSOperationQueue中,就会自动启动,不需要再自己启动了addOperation 内部调用 start方法
start方法 内部调用 main方法
3.NSOperation和NSOperationQueue结合使用创建多线程
1 | 注:这里使用NSBlockOperation示例,其他两种方法一样 |
4.NSOperation和NSOperationQueue的重要属性和方法
NSOperation
NSOperation的依赖 – (void)addDependency:(NSOperation *)op;
1 | // 操作op1依赖op5,即op1必须等op5执行完毕之后才会执行 |
NSOperation操作监听void (^completionBlock)(void)
1 | // 监听操作的完成 |
NSOperationQueue
maxConcurrentOperationCount
1 | //1.创建队列 |
suspended
1 | //当值为YES的时候暂停,为NO的时候是恢复 |
-(void)cancelAllOperations;
1 | //取消所有的任务,不再执行,不可逆 |
注意:暂停和取消只能暂停或取消处于等待状态的任务,不能暂停或取消正在执行中的任务,必须等正在执行的任务执行完毕之后才会暂停,如果想要暂停或者取消正在执行的任务,可以在每个任务之间即每当执行完一段耗时操作之后,判断是否任务是否被取消或者暂停。如果想要精确的控制,则需要将判断代码放在任务之中,但是不建议这么做,频繁的判断会消耗太多时间
5.NSOperation和NSOperationQueue的一些其他属性和方法
NSOperation
1 | // 开启线程 |
NSOperationQueue
1 | // 获取队列中的操作 |
6.NSOperation线程之间的通信
NSOperation线程之间的通信方法
1 | // 回到主线程刷新UI |
我们同样使用下载多张图片合成综合案例
1 |
|
三、GCD的实现
1.任务和队列
GCD中有2个核心概念:任务和队列
任务:执行什么操作,任务有两种执行方式: 同步函数 和 异步函数,他们之间的区别是
同步:只能在当前线程中执行任务,不具备开启新线程的能力,任务立刻马上执行,会阻塞当前线程并等待 Block中的任务执行完毕,然后当前线程才会继续往下运行
异步:可以在新的线程中执行任务,具备开启新线程的能力,但不一定会开新线程,当前线程会直接往下执行,不会阻塞当前线程
队列:用来存放任务,分为串行队列 和 并行队列
串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效
GCD默认提供三种队列
- main queue
- global queue
- dispatch_queue_t
2.GCD的创建
队列的创建
1 | // 第一个参数const char *label : C语言字符串,用来标识 |
创建并发队列
1 | dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT); |
创建串行队列
1 | dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL); |
GCD默认已经提供了全局并发队列,供整个应用使用,可以无需手动创建
1 | /** |
获得主队列
1 | dispatch_queue_t queue = dispatch_get_main_queue(); |
任务的执行
队列在queue中,任务在block块中
开启同步函数 同步函数:要求立刻马上开始执行
1 | /* |
开启异步函数 异步函数 :等主线程执行完毕之后,回过头开线程执行任务
1 | dispatch_async(queue, ^{ |
任务和队列的组合
任务:同步函数 异步函数
队列:串行 并行
异步函数+并发队列:会开启新的线程,并发执行
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
异步函数+串行队列:会开启一条线程,任务串行执行
1 | dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL); |
同步函数+并发队列:不会开线程,任务串行执行
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
同步函数+串行队列:不会开线程,任务串行执行
1 | dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL); |
异步函数+主队列:不会开线程,任务串行执行
使用主队列(跟主线程相关联的队列)
主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行
1 | //1.获得主队列 |
同步函数+主队列:死锁
1 | //1.获得主队列 |
同步函数和异步函数的执行顺序
同步函数:立刻马上执行,会阻塞当前线程
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
异步函数:当前线程会直接往下执行,不会阻塞当前线程
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
3.GCD线程间的通信
1 |
|
GCD线程间的通信非常简单,使用同步或异步函数,传入主队列即可。
4.GCD其他常用函数
栅栏函数dispatch_barrier_async
1 | dispatch_barrier_async(queue, ^{ |
我们来看一下栅栏函数的作用
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
栅栏函数可以控制任务执行的顺序,栅栏函数之前的执行完毕之后,执行栅栏函数,然后在执行栅栏函数之后的
延迟执行
1 | /* |
延迟执行的其他方法:
1 | // 2s中之后调用run方法 |
一次性代码
1 | -(void)once |
一次性代码主要应用在单例模式中,关于单例模式详解大家可以去看iOS-单例模式写一次就够了这里不在赘述。
快速迭代dispatch_apply
1 | /* |
快速迭代:开启多条线程,并发执行,相比于for循环在耗时操作中极大的提高效率和速度
该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。
队列组dispatch_group
1 | // 创建队列组 |
下面看一了实例使用group下载两张图片然后合成在一起
1 |
|
由于篇幅问题,文中有很多方法没有具体解释。后期会在其它文章中做具体说明。