引导
谈到iOS多线程,想必大家第一反应就是多线程4种实现方案 1.pthread
、2.NSThread
、3.GCD
、4.NSOperation
;它们每个的用法、特点、应用场景及注意点,文章会一一讲到。
本篇文章主要从【iOS多线程模块】学习总结,
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。
目录:
- 多线程相关概念
1.进程和线程概念
2.多线程概念
3.主线程
4.GCD相关概念- pthread & NSThread
1.pthread
2.NSThread
1> NSThread创建线程有3个方法
2> NSThread其它方法
3> NSThread线程安全
4> NSThread线程间通信
5> NSThread线程状态转换- GCD中枢调度器
1.什么是GCD
2.GCD基本概念
3.任务&队列组合使用
4.GCD的优势
5.GCD基本使用
6.GCD常见用法和应用场景
7.内存和安全
8.单例模式
9.总结- NSOperation操作队列
1.什么是NSOperation
2.NSOperation相关概念
3.NSInvocationOperation & NSBlockOperation
4.NSOperation优势
5.NSOperation基本使用
6.NSOperation结合NSOperationQueue使用
7.非主队列控制串行和并行执行的关键
8.添加操作依赖和操作监听
9.NSOperation线程间通信
10.管理操作:是操作队列的方法
多线程相关概念篇
1.进程
进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
2.线程
基本概念:1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程),线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。
线程的串行:1个线程中的任务的执行是串行(按顺序地执行),如果要在1个线程中执行多个任务,那么只能一个一个按顺序执行这些任务(也就是说,在同一时间内,1个线程只能执行一个任务)
3.进程和线程的比较
1、进程是CPU分配资源和调度的单位
2、线程是CPU调用(执行任务)的最小单位
3、一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个线程
4、同一个进程内的线程共享进程的资源
多线程概念
多线程概念:即1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务
多线程并发执行:在同一时间里,CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
。
多线程优缺点:优点
:
1、能适当提高程序的执行效率
2、能适当提高资源利用率(CPU、内存利用率)缺点
:
1、开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
2、线程越多,CPU在调度线程上的开销就越大
3、程序设计更加复杂:比如线程之间的通信、多线程的数据共享
主线程
主线程:程序运行后,默认会开启1条线程
作用:刷新显示UI,处理UI事件
使用注意:
1、不要将耗时操作放到主线程中去处理,会卡住线程,严重影响UI界面的流畅度,给用户界面卡顿的现象(放到子线程中执行);
2、和UI相关的刷新操作必须放到主线程中进行处理
多线程实现方案 | 特点 | 语言 | 频率 | 线程生命周期 |
---|---|---|---|---|
pthread | 1、一套通用的多线程API 2、适用于Unix\Linux\Windows等系统 3、跨平台\可移植 4、使用难度大 | c语言 | 几乎不用 | 由程序员进行管理 |
NSThread | 1、使用更加面向对象 2、简单易用,可直接操作线程对象 | OC语言 | 偶尔使用 | 由程序员进行管理 |
GCD | 1、旨在替代NSThread等线程技术 2、充分利用设备的多核(自动) | C语言 | 经常使用 | 自动管理 |
NSOperation | 1、基于GCD(底层是GCD) 2、比GCD多了一些更简单实用的功能 3、使用更加面向对象 | OC语言 | 经常使用 | 自动管理 |
GCD相关概念
初学GCD的时候,肯定会纠结一些看似很关键但却毫无意义的问题(不要纠结,概念理解的基础+实战,就会解决你的疑惑),
对于GCD需要关注的只有两个概念:任务、队列
。
1.任务
linux内核中的任务的定义是描述进程的一种结构体,而GCD中的任务只是一个代码块,它可以指一个block或者函数指针。根据这个代码块添加进入队列的方式,将任务分为异步任务和同步任务:异步任务
:
使用dispatch_async将任务加入队列。将异步任务加入并发队列,会开启多条线程且任务是并发执行(这也是我们最常用的一种方式);将异步任务加入串行队列,会开启一条线程且任务是串行执行(按顺序执行);将异步任务加入主队列,不会开启线程且任务都在主线程中执行。同步任务
:
使用dispatch_sync将任务加入队列。将同步任务加入并发队列,不会开启线程且任务是串行执行;将同步任务加入串行队列,不会开启线程且任务是串行执行(也没什么意义是吧);将同步任务加入主队列,不会开启线程且任务都在主线程中执行(注意:方法在主线程调用会造成死锁,在子线程中调用不会造成死锁)。
2.队列
调度队列是一个对象,它会以first-in、first-out的方式管理您提交的任务。GCD有三种队列类型并行队列
:
并发队列虽然是能同时执行多个任务,但这些任务仍然是按照先到先执行(FIFO)的顺序来执行的。并发队列会基于系统负载来合适地选择并发执行这些任务。并发队列一般指的就是全局队列(Global queue),进程中存在四个全局队列:高、中(默认)、低、后台四个优先级队列,可以调用dispatch_get_global_queue函数传入优先级来访问队列。当然我们也可以用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_CONCURRENT,来自己创建一个并发队列。
串行队列
:
串行队列将任务以先进先出(FIFO)的顺序来执行,所以串行队列经常用来做访问某些特定资源的同步处理。你可以也根据需要创建多个队列,而这些队列相对其他队列都是并发执行的。换句话说,如果你创建了4个串行队列,每一个队列在同一时间都只执行一个任务,对这四个任务来说,他们是相互独立且并发执行的。如果需要创建串行队列,一般用dispatch_queue_create这个方法来实现,并指定队列类型DISPATCH_QUEUE_SERIAL。
主队列
:
主队列,与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
注:队列间的执行是并行的,但是也存在一些限制。比如,并行执行的队列数量受到内核数的限制,无法真正做到大量队列并行执行;比如,对于并行队列中的全局队列而言,其存在优先级关系,执行的时候也会遵循其优先顺序,而不是并行。
以上概念文言文你也许感到有点什么,下面总结简单小表格方便你查看
3.GCD总结小表格
GCD | 特点 |
---|---|
核心概念 | 任务:执行什么操作 |
队列:用来存放任务 | |
函数 | 异步:可以在新的线程中执行任务,具备开启新线程的能力 |
同步:只能在当前线程中执行任务,不具备开启新线程的能力 | |
队列 | 并发:允许多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步函数下才有效 |
串行:一个任务执行完毕后,再执行下一个任务(按顺序执行) | |
全局并发队列 | 特点:存在优先级关系(DEFAULT默认的、HIGH高的、LOW低的、BACKGROUND最低的) |
主队列 | 特点:添加到主队列上的任务,必须在主线程执行。如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务,直到主线程空闲为止 |
对于 任务与队列 之间的关系,下面总结简单小表格方便你查看
任务 & 队列 | 并发队列(concurrent) | 串行队列(serial) | 主队列(get_main) |
---|---|---|---|
异步函数(async) | 会开启多条线程,队列中的任务是并发执行 | 会开启一条线程,队列中的任务是串行执行 | 不会开启线程,所有任务都在主线程中执行 |
同步函数(sync) | 不会开启线程,队列中的任务是串行执行 | 不会开启线程,队列中的任务是串行执行 | 不会开启线程,所有任务都在主线程中执行(注意:在主线程调用会造成死锁,在子线程中调用不会造成死锁) |
pthread & NSThread篇
1.pthread
其实这个方案开发几乎不用,只是拿来充个数,为让大家了解一下就好了
简单介绍下,pthread是一套通用的多线程的API,可以在Unix / Linux / Windows 等系统跨平台使用,使用C语言编写,需要程序员自己管理线程的生命周期,使用难度较大,所以仅了解,看一遍有个印象。
pthread使用方法
打印输出:
应用场景:我们在iOS开发中几乎不使用pthread
2.NSThread
这个方案是经过苹果封装后的,使用更加面向对象,简单易用可直接操作线程对象,但是,它的生命周期还是需要我们手动管理,所以这个方案也是偶尔用用,比如 [NSThread currentThread]
用来获得当前线程类,你就可以知道当前线程的各种属性,用于调试十分方便。下面来看看它的一些用法
1.NSThread创建线程有3个方法
首先要包含头文件#import <pthread.h>
方法一:创建线程且手动启动
方法二:分离子线程并自动启动
方法三:后台线程并自动启动
2.NSThread其他方法
除了创建启动外,NSThread 还以很多方法,下面我列举一些常见的方法,当然我列举的并不完整,更多方法大家可以去类的定义里去看
2.NSThread线程安全
线程安全,解决方法采用线程加锁,需了解互斥锁
互斥锁使用格式:@synchronized (self) {// 需要锁定的代码 }
注意:锁定一份代码只用一把锁,用多把锁是无效的
互斥锁的优缺点:优点
:能有效防止因多线程抢夺资源造成的数据安全问题缺点
:需要消耗大量的CPU资源
互斥锁注意点:
锁:必须是全局唯一的(通常用self)
1.注意加锁的位置
2.注意加锁的前提条件,多线程共享同一块资源
3.注意加锁是需要代价的,需要耗费性能的
4.加锁的结果:线程同步(按顺序执行)
补充:
我们知道, 属性中有atomic和nonatomic属性
atomic
: setter方法线程安全, 需要消耗大量的资源
nonatomic
: setter方法非线程安全, 适合内存小的移动设备
3.NSThread线程间通信
线程间通信:任务从子线程回到主线程
4.NSThread线程状态转换
GCD中枢调度器
1.什么是GCD
GCD全称 Grand Central Dispath,可译为”强大的中枢调度器”,基于libdispatch 纯C语言,里面包含了许多多线程相关非常强大的函数. 程序员可以既不写一句线程管理的代码又能很好地使用多线程执行任务。
2.GCD基本概念
初学GCD的时候,肯定会纠结一些看似很关键但却毫无意义的问题(不要纠结,概念理解的基础+实战,就会解决你的疑惑),
对于GCD需要关注的只有两个概念:任务 & 队列
。
而对于GCD相关的概念解读,如果你对这些都已有了解 可以忽略,如果还有疑虑可以参考一下 「iOS多线程—夯实基础「多线程基本概念」
在这里就提供 GCD主要概念简单总结小表格,方便你查看。
GCD | 特点 |
---|---|
核心概念 | 任务:执行什么操作 |
队列:用来存放任务 | |
函数 | 异步:可以在新的线程中执行任务,具备开启新线程的能力 |
同步:只能在当前线程中执行任务,不具备开启新线程的能力 | |
队列 | 并发:允许多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步函数下才有效 |
串行:一个任务执行完毕后,再执行下一个任务(按顺序执行) | |
全局并发队列 | 特点:存在优先级关系(DEFAULT默认的、HIGH高的、LOW低的、BACKGROUND最低的) |
主队列 | 特点:添加到主队列上的任务,必须在主线程执行。如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务,直到主线程空闲为止 |
3.任务&队列组合使用
下面总结简单小表格方便你查看
任务 & 队列 | 并发队列(concurrent) | 串行队列(serial) | 主队列(get_main) |
---|---|---|---|
异步函数(async) | 会开启多条线程,队列中的任务是并发执行 | 会开启一条线程,队列中的任务是串行执行 | 不会开启线程,所有任务都在主线程中执行 |
同步函数(sync) | 不会开启线程,队列中的任务是串行执行 | 不会开启线程,队列中的任务是串行执行 | 不会开启线程,所有任务都在主线程中执行(注意:在主线程调用会造成死锁,在子线程中调用不会造成死锁) |
4.GCD的优势
易用: GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱,而是因为基于block,它能极为简单得在不同代码作用域之间传递上下文。
灵活: GCD 具有在常见模式(比如互斥锁、单例模式)上的特点,且会自动管理线程的生命周期创建线程、调度任务、销毁线程),用更高性能的方法优化代码,而且GCD(C API)能提供更多的控制权力以及大量的底层函数。
性能: GCD 会自动利用更多的CPU内核(比如双核、四核),且自动根据系统负载来增减线程数量,这就减少了线程间切换以及增加了计算效率。
怎么样? 心动不, 迫不及待想要知道怎么使用GCD了吧, 那我们正式投入GCD的怀抱了~
我会通过代码展示。
5.GCD基本使用
|
|
6.GCD常见用法和应用场景
1.dispatch_async 异步函数
使用方法:(线程间通信)
应用场景:
这种用法非常常见,比如开启一个异步的网络请求,待数据返回后返回主队列刷新UI;又比如请求图片,待图片返回刷新UI或是耗时文件操作等等。
2.dispatch_after 延迟执行
使用方法:(多个方法,好对比)
应用场景:
这为我们提供了一个简单的延迟执行的方式,比如在view加载结束延迟执行一个动画等等
3.dispatch_time 延迟时间
使用方法:
4.dispatch_once_t 一次性代码
使用方法:(保证某段代码在程序运行过程中只被执行1次)
应用场景:
可以使用其创建一个单例,也可以做一些其他只执行一次的代码,注意:看到一次性代码你可能会想到懒加载,提醒
dispatch_once_t 不能放在懒加载中的
5.dispatch_barrier_async 栅栏函数
使用方法:
栅栏函数使用全局并发队列,打印输出
栅栏函数使用手动创建并发队列,打印输出
应用场景:
和dispatch_group类似,dispatch_barrier也是异步任务间的一种同步方式,可以在比如文件的读写操作时使用,保证读操作的准确性。注意:dispatch_barrier_async只在自己创建的并发队列上有效。
6.dispatch_apply 快速迭代
使用方法:
打印输出:
应用场景:
dispatch_apply并行的运行机制,效率一般快于for循环的类串行机制(在for一次循环中的处理任务很多时差距比较大)。比如这可以用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性,如果用for循环,耗时较多,并且每个表单的数据没有依赖关系,所以用dispatch_apply比较好。
7.dispatch_group_t 队列组
使用方法:
group_notify 打印输出:
group_wait 打印输出:
注意:group_notify
拦截通知,当队列组中所有的任务都执行完毕的时候回进入到这个方法,问题? 该方法是阻塞的吗? –> 内部本身是异步的。group_wait
等待.死等. 直到队列组中所有的任务都执行完毕之后才能执行,(DISPATCH_TIME_NOW : 现在,DISPATCH_TIME_FOREVER : 永远) 作用:阻塞。
8.dispatch_sync 死锁
怎样会造成死锁 & 如何避开死锁
使用方法:
打印输出:
下面这种情况,也会造成死锁
9.dispatch_suspend&dispatch_resume 挂起队列和恢复队列
使用方法:
应用场景:
有时候,我们不想让队列中的某些任务马上执行,这时我们可以通过挂起操作来阻止一个队列中将要执行的任务。
注意:执行挂起操作不会对已经开始执行的任务起作用,它仅仅只会阻止将要进行但是还未开始的任务。
7.内存和安全
内存
MRC:用dispatch_retain
和dispatch_release
管理dispatch_object_t
内存。
ARC:ARC在编译时刻自动管理dispatch_object_t
内存,使用retain
和release
会报错。
安全dispatch_queue
是线程安全的,你可以随意往里面添加任务。
补充
1.注意ARC不是垃圾回收机制,是编译器特性
配置MRC环境:build setting
->搜索automatic ref
->修改为NO
2.在MRC环境下,如果用户retain
了一次,那么直接返回instance
变量,不对引用计数器+1
如果用户release
了一次,那么什么都不做,因为单例模式在整个程序运行过程中都拥有且只有一份,程序退出之后被释放,所以不需要对引用计数器操作
8.单例模式
单例也就是在程序的整个生命周期中, 该类有且仅有一个实例对象, 此时为了保证只有一个实例对象, 我们这里用到了dispatch_once
函数
在这里我就整理好吧,就不直接粘上代码了,可能会很多地方用到,到时会很麻烦。下面整理了单例模式通用的宏
,如果你需要可以直接拷走,是吧~
9.总结
GCD可进行线程间通信
GCD可以办到线程安全
GCD可用于延迟执行
GCD需要注意死锁问题(不要在当前队列调用同步函数)
NSOperation操作队列篇
1.什么是NSOperation
NSOperation
是苹果提供给我们的一套多线程解决方案。实际上NSOperation
是基于GCD 的封装,完全面向对象,但是比GCD更简单易用、代码可读性也更高,使用也更好理解。使用起来也和GCD差不多,其中 NSOperation
相当于GCD中的任务,而NSOperationQueue
则相当于GCD中的队列。
注意:NSOperation
需要配合NSOperationQueue
来实现多线程(1.将要操作任务封装到一个
NSOperation对象中,2.将此任务添加到一个
NSOperationQueue对象中
,然后系统就会自动在执行任务)。因为默认情况下,NSOperation
单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue
才能实现异步执行。
2.NSOperation相关概念
并行(Concurrent) & 串行(Serial)
并行和串行描述的是任务和任务之间的执行方式,并行是任务A和任务B可以同时执行,串行是任务A执行完了任务B才能执行(按顺序执行)。异步(Asynchronous) & 同步(Synchronous)
异步和同步描述的其实就是函数什么时候返回. 比如用来下载图片的函数: 同步函数只有在image下载结束之后才返回, 下载的这段时间函数只能等待,而异步函数,不会去等它完成(异步函数不会堵塞当前线程去执行下一个函数)。并发(Concurrency) & 并行(Parallelism)
这个更容易混淆了, 并发和并行都是用来让不同的任务可以”同时执行”。 只是并行是真同时,而并发是假同时(是CPU地在各个进程之间快速切换, 给人一种能同时处理多任务的错觉)。
3.NSInvocationOperation& NSBlockOperation
NSOperation
是一个抽象类,它不能直接使用,所以你必须使用NSOperation
子类,系统已经给我们封装了NSBlockOperation
和 NSInvocationOperation
这两个实体类,不过我们更多的使用是自己继承并定制自己的操作。
1.NSInvocationOperation
:使用这个类来初始化一个操作,它包括指定对象的调用selector
。
2.NSBlockOperation
:使用这个类来用一个或多个block初始化操作,操作本身可以包含多个块。当所有block被执行操作将被视为完成。
4.NSOperation优势
NSOperation
是基于GCD的封装, 拥有更多的API(suspended, cancelled).- 在
NSOperationQueue
中, 可以指定各个NSOperation
之间的依赖关系(注意:不能相互依赖). - 用KVO可以方便的监测
NSOperation
的状态(isExecuted, isFinished, isCancelled). - 更高的可定制能力, 你可以继承
NSOperation
实现可复用的逻辑模块.
5.NSOperation基本使用
在不使用NSOperationQueue,需要调用 start 方法来启动任务,不开启新线程,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。
1.NSInvocationOperation
这里,NSBlockOperation
还有一个方法:addExecutionBlock:
(追加任务),通过这个方法可以给 NSBlockOperation
添加多个执行 Block
。额外操作中的任务 会开子线程并发执行任务(这里可能是子线程也可能是主线程执行
) 注意下面的打印结果:
打印输出:
3.自定义子类继承NSOperation,实现内部相应的方法(重写main)
打印输出:
6.NSOperation结合NSOperationQueue使用
NSOperationQueue 按类型来说共有两种类型:主队列、其他队列。只要添加到队列,会自动调用任务的 start 方法。
1.主队列
凡是添加到主队列中的任务(NSOperation),都会放到主线程中 串行执行。NSOperationQueue *queue1 = [NSOperationQueue mainQueue];
2.其他队列(非主队列)
添加到这种队列中的任务(NSOperation),就会自动放到子线程中 并发执行,同时具有:串行、并发功能。NSOperationQueue *queue = [[NSOperationQueue alloc] init];
结合使用:
1.添加任务到队列中 addOperation:
使用非主队列打印输出:
使用主队列打印输出:
2.添加任务到队列中 addOperationWithBlock:
,简易方法:
打印输出:
7.非主队列控制串行和并行执行的关键
NSOperationQueue创建的其他队列 同时具有串行、并发功能,上边我们演示了并发功能,那么下面讲讲串行功能,这里有个关键参数:maxConcurrentOperationCount
队列最大并发数(同一时间最多几个任务可以执行)
误区:串行执行任务不等于只开一条线程(线程同步,要看任务的执行方式是顺序还是并发的)
maxConcurrentOperationCount | 描述 |
---|---|
.> 1 | 并发队列 |
= 1 | 串行队列 |
= 0 | 不会执行任务 |
= -1 | 特殊意义,最大值表示不受限制 |
|
|
最大并发数 =1,打印输出:
最大并发数 =2,打印输出:
8.添加操作依赖和操作监听
NSOperation 有一个非常实用的功能,那就是 添加依赖addDependency:
(也可以跨队列依赖),注意:这里不能相互依赖。
只有所有依赖的对象都已经完成操作,当前NSOperation对象才会开始执行操作。需要先添加依赖关系,再将操作添加到队列中。另外,通过removeDependency方法来删除依赖对象。
给操作任务 添加监听addExecutionBlock:
,当任务完成后就会,走到这个Block块里面,
具体怎么添加依赖和监听如下:
打印输出:
9.NSOperation线程间通信
|
|
打印输出:
10.管理操作:是操作队列的方法
队列中的任务也是有状态:已经执行完成的、正在执行、等待执行
应用场景:提高用户体验第一,当用户操作时,取消一切跟用户当前操作无关的进程,提升流畅度。(开始滚动的时候 暂停操作、滑动结束的时候 恢复操作、接收到内存警告 取消所有操作
)
1.添加操作依赖
2.管理操作:重点!是操作队列的方法
- 暂停/恢复 取消操作,(暂停和取消,不能暂停或取消正在执行状态的任务,且取消不可以恢复)
- 开启合适的线程数量!(最多不超过6条)
- 一般开发的时候,会将操作队列设置成一个全局的变量(属性)
方法:
好了,就到这里吧。当然,我讲的并不完整,可能有一些知识我并没有讲到,但作为常用方法,这些已经足够了。不过我在这里只是告诉你了一些方法的功能,只是怎么把他们用到合适的地方,就需要多多实践了,看我写的这么卖力,不打赏的话得点个喜欢也是极好的。
期待
如果在阅读过程中遇到 error || new ideas,希望你能 messages 我,我会及时改正谢谢。
点击右上角的 喜欢 和 订阅Rss 按钮,可以收藏本仓库,并在 Demo 更新时收到邮件通知。