多线程简单介绍
进程和线程
1、什么是进程
进程是指在系统中正在运行的一个应用程序,每个进程之间是相互独立的,每个进程均运行在其专用且受保护的内存空间内。
比如同时打开QQ、Xcode,系统会分别启动2个进程,通过“活动监视器”可以查看Mac系统中所开启的进程。
2、什么是线程
1个进程想要执行任务,必须得有线程(每一个进程至少要有一条线程即:主线程),线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行,比如使用音乐播放器播放音乐,使用下载器下载电影,都需要在线程中执行。
3、线程的串行
1个线程中任务的执行是串行的,如果要在1个线程中执行多个任务,那么只能一个一个的按照顺序执行这些任务。也就是说,在同一时间内,1个线程只能执行1个任务。比如在1个线程中下载3个文件(分别是文件A、文件B、文件C)。
多线程
1、什么是多线程
1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。进程->车间,线程->车间工人。多线程技术可以提高程序的执行效率。比如同时开启3条线程分别下载3个文件(分别是文件A、文件B、文件C)。
2、多线程原理
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速的在多线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
思考:如果线程非常非常多,会发生什么情况?
CPU会在N多个线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频率会降低(线程的执行效率降低)。
3、多线程的优缺点
多线程的优点:
能适当提高程序的执行效率
能适当的提高资源的利用率(CPU、内存利用率)
多线程的缺点:
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512k),如果开启大量的线程,会占用大量的内存空间,CPU在调度线程上的开销就越大。程序设计更加复杂:比如线程之间的通信、多线程的数据共享。
4、多线程在iOS开发中的应用
主线程:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”。
主线程的主要作用:显示/刷新UI界面;处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意:别将比较耗时的操作放到主线程中。耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的体验
5、代码示例
1 | #import "ViewController.h" |
执行效果:
说明:当点击按钮的时候,textView点击没反应
执行分析:等待主线程串行执行。
开启子线程
线程安全
多线程的安全隐患
资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。比如多个线程访问同一个对象、同一个变量、同一个文件。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
示例一:
示例二:
问题代码:
1 | import "ViewController.h" |
打印结果:
安全隐患分析
如何解决
互斥锁使用格式
1 | @synchronized(锁对象){ |
注意:锁定1份代码只用1把锁,用多把锁是无效的
代码示例:
1 | #import "ViewController.h" |
执行效果图:
互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
互斥锁的使用前提:多条线程抢夺同一块资源
相关专业术语:线程同步,多条线程按照顺序的执行任务。互斥锁,就是使用了线程同步技术。
原子和非原子属性
OC在定义属性时有nonatomic和atomic两种选择
atomic:源自属性,为setter方法加锁(默认就是atomic)
nonatomic:非原子属性,不会为setter方法加锁
atomic加锁原理:
1 | @property(assign, atomic) int age; |
原子和非原子属性的选择:
nonatomic和atomic对比
atomic:线程安全,需要消耗大量的资源
nonatomic:非线程安全,适合内存小的移动设备
iOS开发的建议:
所有的属性都声明为nonatomic
尽量避免多线程抢夺同一块资源
尽量加锁、资源抢夺的业务逻辑交给服务器处理,减小移动客户端的压力
线程间的通信
简单说明
线程间通信:在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信。
线程通信间的体现:1个线程传递数据给另1个线程;在1个线程中执行完特定任务后,转到另1个线程继续执行任务。
线程间通信常用方法:
1 | - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait; |
线程间通信示例-图片下载
代码1:
1 | #import "ViewController.h" |
代码2:
1 | #import "ViewController.h" |
多线程之[pthread、NSThread]
pthread
pthread简单介绍一下,pthread是一套通用的多线程API,可以在Unix/Linux/Windows等系统跨平台使用,使用C语言编写,需要程序员自己管理线程的生命周期,使用难度较大,所以我们在iOS开发中几乎不使用pthread。
引自百度百科
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32.引自维基百科
POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。
实现POSIX线程标准的库常被称为Pthreads,一般基于Unix-like POSIX系统,如Linux、Solaris。但是Microsoft Windows上的实现也存在,例如直接使用Windows API实现的第三方库pthreads-w32;而利用windows的SFU/SUA子系统,则可以使用微软提供的一部分原生POSIX API。
pthread的使用方法
- 首先要包含头文件
#import <pthread.h>
- 其次要创建线程,并开启线程执行任务
1 | //创建线程:定义一个pthread_t类变量 |
pthread_create(&thread, NULL, run, NULL);
中各项参数含义:
- 第一个参数&thread是线程对象
- 第二个和第四个是线程属性,可赋值NULL
- 第三个run表示指向函数的指针(run对应函数里是需要在新线程中执行的任务)
NSThread
NSThread是苹果官方提供的,使用起来比pthread更加面向对象,简单易用,可以直接操作线程对象。不过也需要程序自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用NSThread。比如我们会经常调用[NSThread currentThread]
来显示当前的进程信息。
下边我们说说NSThread如何使用
1、创建、启动线程
- 先创建线程,再启动线程
1 | NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; |
- 创建线程后自动启动线程
1 | [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; |
- 隐式创建并启动线程
1 | [self performSelectorInBackground:@selector(run) withObject:nil]; |
2、线程相关用法
1 | //获得主线程 |
3、线程状态控制方法
- 启动线程方法
1 | - (void)start; |
- 阻塞(暂停)线程方法
1 | + (void)sleepUntilDate:(NSDate *)date; |
- 强制停止线程
1 | + (void)exit; |
4、线程的状态转换
当我们新建一条线程NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
,在内存中表现为:
当调用[thread start];
后,系统把线程对象放入可调度线程池中,线程对象进入就绪状态,如下图所示:
当然,可调度线程池中,会有其他的线程对象,如下图所示:(在这里我们只关心左边的线程对象)
下边我们来看看当前线程的状态转换:
- 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
- 如果CPU在运行当前线程对象的时候调用了sleep方法/等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时/得到同步锁,则回到就绪状态。
- 如果CPU在运行当前线程对象的时候,线程任务执行完毕/异常强制退出,则当前线程对象进入死亡状态。
只看文字可能不太好理解,具体当前线程对象的状态变化如下图所示:
GCD
GCD简介
什么是GCD呢?我们先来看看百度百科的解释简单了解下概念
引自百度百科
Grand Central Dispatch(GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的任务。在Mac OS X10.6雪豹中首次推出的,也可以在iOS4及以上版本使用
为什么要用GCD呢?
因为GCD有很多好处啊,具体如下:
- GCD可用于多核的并行运算
- GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
既然GCD有这么多的好处,那下面我们就来系统的学习一下GCD的使用方法。
任务和队列
学习GCD之前,先来了解GCD中两个核心概念:任务和队列。
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的。执行任务有两种方式:同步执行和异步执行。两者的主要区别是:是否具有开启新线程的能力。
- 同步执行(sync):只能在当前线程中执行任务,不具备开启新线程的能力。
- 异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力。
队列:这里的队列指任务队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。在GCD中有两种队列:串行队列和并行队列。
并行队列(Concurrent Dispatch Queue):可以让多个任务并行(同时)执行(自动开启多个线程同时执行任务)。
并行功能只有在异步(dispatch_async)函数下才有效
串行队列(Serial Dispatch Queue):让任务一个接一个的执行(一个任务执行完毕后,再执行下一个任务)
GCD的使用步骤
GCD的使用步骤其实很简单,只有两步。
- 创建一个队列(串行队列或者并行队列)
- 将任务添加到队列中,然后系统就会根据任务类型执行任务(同步执行或者异步执行)
下边来看看队列的创建方法和任务的创建方法
1、队列的创建方法
- 可以使用
dispatch_queue_create
来创建对象,需要传入两个参数,第一个参数表示队列的唯一标识符,用于debug,可为空;第二个参数用来识别是串行队列还是并行队列。DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并行队列。
1 | //串行队列的创建方法 |
- 对于并行队列,还可以使用
dispatch_get_global_queue
来创建全局并行队列。GCD默认提供了全局的并行队列,需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个参数暂时没用,用0表示即可。
2、任务的创建方法
1 | //同步执行任务创建方法 |
虽然使用GCD只需要两步,但是既然我们有两种队列,两种任务执行方式,那么我们就有了4种不同的组合方式。这四种不同的组合方式是:
1、并行队列 + 同步执行
2、并行队列 + 异步执行
3、串行队列 + 同步执行
4、串行队列 + 异步执行
实际上,我们还有一种特殊的队列是主队列,那样就有6种不同的组合方式了。
5、主队列 + 同步执行
6、主队列 + 异步执行
那么这几种不同组合方式各有什么区别呢?这里为了方便,先上结果,再来讲解。为图省事儿,直接查看表格结果。
并行队列 | 串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 |
异步(async) | 有开启新线程,并行执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
下边我们来分别看看那这几种组合方式的使用方法
GCD的基本使用
并行队列的两种使用方法:
1、并行队列 + 同步执行
- 不会开启新线程,执行完一个任务,再执行下一个任务
1 | - (void)syncConcurrent { |
输出结果:
2016-09-03 19:22:27.577 GCD[11557:1897538] syncConcurrent—begin
2016-09-03 19:22:27.578 GCD[11557:1897538] 1——<NSThread: 0x7f82a1d058b0>{number = 1, name = main}
2016-09-03 19:22:27.578 GCD[11557:1897538] 1——<NSThread: 0x7f82a1d058b0>{number = 1, name = main}
2016-09-03 19:22:27.578 GCD[11557:1897538] 2——<NSThread: 0x7f82a1d058b0>{number = 1, name = main}
2016-09-03 19:22:27.579 GCD[11557:1897538] 2——<NSThread: 0x7f82a1d058b0>{number = 1, name = main}
2016-09-03 19:22:27.579 GCD[11557:1897538] 3——<NSThread: 0x7f82a1d058b0>{number = 1, name = main}
2016-09-03 19:22:27.579 GCD[11557:1897538] 3——<NSThread: 0x7f82a1d058b0>{number = 1, name = main}
2016-09-03 19:22:27.579 GCD[11557:1897538] syncConcurrent—end
- 从并行队列 + 同步执行中可以看到,所有任务都是在主线程中执行的。由于只有一个线程,所以任务只能一个一个的执行。
- 同时我们还可以看到,所有任务都在打印的syncConcurrent—begin和syncConcurrent—end之间,这说明任务是添加到队列中么马上执行的。
2、并行队列 + 异步执行
- 可同时开启多线程,任务交替完成
1 | - (void)asyncConcurrent { |
输出结果:
2016-09-03 19:27:31.503 GCD[11595:1901548] asyncConcurrent—begin
2016-09-03 19:27:31.504 GCD[11595:1901548] asyncConcurrent—end
2016-09-03 19:27:31.504 GCD[11595:1901626] 1——<NSThread: 0x7f8309c22080>{number = 2, name = (null)}
2016-09-03 19:27:31.504 GCD[11595:1901625] 2——<NSThread: 0x7f8309f0b790>{number = 4, name = (null)}
2016-09-03 19:27:31.504 GCD[11595:1901855] 3——<NSThread: 0x7f8309e1a950>{number = 3, name = (null)}
2016-09-03 19:27:31.504 GCD[11595:1901626] 1——<NSThread: 0x7f8309c22080>{number = 2, name = (null)}
2016-09-03 19:27:31.504 GCD[11595:1901625] 2——<NSThread: 0x7f8309f0b790>{number = 4, name = (null)}
2016-09-03 19:27:31.505 GCD[11595:1901855] 3——<NSThread: 0x7f8309e1a950>{number = 3, name = (null)}
- 在并行队列 + 异步执行中可以看出,除了主线程,又开启了3个线程,并且交替着同时执行。
- 另一方面可以看出,所有任务是在打印的asyncConcurrent—begin 和 asyncConcurrent—end之后才开始执行的。说明任务不是马上执行的,而是将所有任务添加到队列之后才开始异步执行的。
接下来看看串行队列的执行方法。
3、串行队列 + 同步执行
- 不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务
1 | - (void)syncSerial{ |
输出结果为:
2016-09-03 19:29:00.066 GCD[11622:1903904] syncSerial—begin
2016-09-03 19:29:00.067 GCD[11622:1903904] 1——<NSThread: 0x7fa2e9f00980>{number = 1, name = main}
2016-09-03 19:29:00.067 GCD[11622:1903904] 1——<NSThread: 0x7fa2e9f00980>{number = 1, name = main}
2016-09-03 19:29:00.067 GCD[11622:1903904] 2——<NSThread: 0x7fa2e9f00980>{number = 1, name = main}
2016-09-03 19:29:00.067 GCD[11622:1903904] 2——<NSThread: 0x7fa2e9f00980>{number = 1, name = main}
2016-09-03 19:29:00.067 GCD[11622:1903904] 3——<NSThread: 0x7fa2e9f00980>{number = 1, name = main}
2016-09-03 19:29:00.068 GCD[11622:1903904] 3——<NSThread: 0x7fa2e9f00980>{number = 1, name = main}
2016-09-03 19:29:00.068 GCD[11622:1903904] syncSerial—end
- 在串行队列 + 同步执行可以看到,所有任务都是在主线程中执行的,并没有开启新的线程。而且由于串行队列,所以按顺序一个一个执行。
- 同时我们还可以看到,所有任务都在打印的syncSerial—begin和syncSerial—end之间,这说明任务是添加到队列中马上执行的。
4、串行队列 + 异步执行
- 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
1 | - (void)asyncSerial{ |
输出结果为:
2016-09-03 19:30:08.363 GCD[11648:1905817] asyncSerial—begin
2016-09-03 19:30:08.364 GCD[11648:1905817] asyncSerial—end
2016-09-03 19:30:08.364 GCD[11648:1905895] 1——<NSThread: 0x7fb548c0e390>{number = 2, name = (null)}
2016-09-03 19:30:08.364 GCD[11648:1905895] 1——<NSThread: 0x7fb548c0e390>{number = 2, name = (null)}
2016-09-03 19:30:08.364 GCD[11648:1905895] 2——<NSThread: 0x7fb548c0e390>{number = 2, name = (null)}
2016-09-03 19:30:08.364 GCD[11648:1905895] 2——<NSThread: 0x7fb548c0e390>{number = 2, name = (null)}
2016-09-03 19:30:08.365 GCD[11648:1905895] 3——<NSThread: 0x7fb548c0e390>{number = 2, name = (null)}
2016-09-03 19:30:08.365 GCD[11648:1905895] 3——<NSThread: 0x7fb548c0e390>{number = 2, name = (null)}
- 在串行队列 + 异步执行可以看到,开启了一个新线程,但是任务还是串行,所以任务是一个一个执行的。
- 另一方面可以看出,所有任务是在打印的asyncSerial—begin 和 asyncSerial—end之后才开始执行的。说明任务不是马上执行,而是将所有任务添加到队列之后才开始同步执行。
下面我们看看特殊的队列—主队列
- 主队列:GCD自带的一种特殊的串行队列
所有放在主队列中的任务,都会放到主线程中执行。
可使用dispatch_get_main_queue()
获得主队列
我们再看看主队列的两种组合方式
5、主队列 + 同步执行
- 互等卡住不可行(在主线程中调用)
1 | - (void)syncMain { |
输出结果
2016-09-03 19:32:15.356 GCD[11670:1908306] syncMain—begin
这时候,我们惊奇的发现,在主线程中使用主队列 + 同步执行,任务不再执行了,而且syncMain—end也没有打印。这是为什么呢?
这是因为我们在主线程中执行这段代码。我们把任务放到了主队列中,也就是放到了主线程的队列中。而同步执行有个特点,就是对于任务是立马执行的。那么当我们把第一个任务放进主队列中,它就会立马执行。但是主线程现在在处理syncMain方法,所以任务需要等syncMain执行完才能执行。而syncMain执行到第一个任务的时候,又要等第一个任务执行完才能往下执行第二个和第三个任务。
那么,现在的情况就是syncMain方法和第一个任务都在等对方执行完毕。这样大家互相等待。所以就卡住了,所以我们的任务执行不了,而且syncMain–end也没有打印。
如果不在主线程中调用,而在其他线程中调用会如何呢?
- 不会开启新线程,执行完一个任务,再执行下一个任务(在其他线程中调用)
1 | dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT); |
输出结果:
2016-09-03 19:32:45.496 GCD[11686:1909617] syncMain—begin
2016-09-03 19:32:45.497 GCD[11686:1909374] 1——<NSThread: 0x7faef2f01600>{number = 1, name = main}
2016-09-03 19:32:45.498 GCD[11686:1909374] 1——<NSThread: 0x7faef2f01600>{number = 1, name = main}
2016-09-03 19:32:45.498 GCD[11686:1909374] 2——<NSThread: 0x7faef2f01600>{number = 1, name = main}
2016-09-03 19:32:45.498 GCD[11686:1909374] 2——<NSThread: 0x7faef2f01600>{number = 1, name = main}
2016-09-03 19:32:45.499 GCD[11686:1909374] 3——<NSThread: 0x7faef2f01600>{number = 1, name = main}
2016-09-03 19:32:45.499 GCD[11686:1909374] 3——<NSThread: 0x7faef2f01600>{number = 1, name = main}
2016-09-03 19:32:45.499 GCD[11686:1909617] syncMain—end
- 在其他线程中使用主队列+同步执行可看到:所有的任务都是在主线程中执行的,并没有开启新的线程。而且由于主队列是串行队列,所以按照顺序一个一个执行。
- 同时我们还可以看到,所有任务都在打印的syncConcurrent—begin和syncConcurrent—end之间,这说明任务是添加到队列中马上执行的。
6、主队列 + 异步执行
- 只在主线程中执行任务,执行完一个任务,再执行下一个任务
1 | - (void)asyncMain { |
输出结果:
2016-09-03 19:33:54.995 GCD[11706:1911313] asyncMain—begin
2016-09-03 19:33:54.996 GCD[11706:1911313] asyncMain—end
2016-09-03 19:33:54.996 GCD[11706:1911313] 1——<NSThread: 0x7fb623d015e0>{number = 1, name = main}
2016-09-03 19:33:54.997 GCD[11706:1911313] 1——<NSThread: 0x7fb623d015e0>{number = 1, name = main}
2016-09-03 19:33:54.997 GCD[11706:1911313] 2——<NSThread: 0x7fb623d015e0>{number = 1, name = main}
2016-09-03 19:33:54.997 GCD[11706:1911313] 2——<NSThread: 0x7fb623d015e0>{number = 1, name = main}
2016-09-03 19:33:54.997 GCD[11706:1911313] 3——<NSThread: 0x7fb623d015e0>{number = 1, name = main}
2016-09-03 19:33:54.997 GCD[11706:1911313] 3——<NSThread: 0x7fb623d015e0>{number = 1, name = main}
- 我们发现所有任务都在主线程中,虽然异步执行,具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中,并且一个接一个执行。
- 另一方面可以看出,所有任务是在打印的asyncMain—begin和asyncMain—end之后才开始执行的。说明任务不是马上执行,而是将所有任务添加到队列之后才开始同步执行。
弄懂了难理解、绕来绕去的队列 + 任务之后,我们来看看一个简单的东西–GCD线程之间的通信
GCD线程之间的通讯
在iOS开发过程中,我们一般在主线程里边进行UI刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其它线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
输出结果:
2016-09-03 19:34:59.165 GCD[11728:1913039] 1——<NSThread: 0x7f8319c06820>{number = 2, name = (null)}
2016-09-03 19:34:59.166 GCD[11728:1913039] 1——<NSThread: 0x7f8319c06820>{number = 2, name = (null)}
2016-09-03 19:34:59.166 GCD[11728:1912961] 2——-<NSThread: 0x7f8319e00560>{number = 1, name = main}
- 可以看到在其他线程中先执行操作,执行完了之后回到主线程执行主线程的相应操作。
GCD其他方法
1、GCD的栅栏方法 dispatch_barrier_async
- 我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到
dispatch_barrier_async
方法在两个操作组间形成栅栏。
1 | - (void)barrier |
输出结果:
2016-09-03 19:35:51.271 GCD[11750:1914724] —-1—–<NSThread: 0x7fb1826047b0>{number = 2, name = (null)}
2016-09-03 19:35:51.272 GCD[11750:1914722] —-2—–<NSThread: 0x7fb182423fd0>{number = 3, name = (null)}
2016-09-03 19:35:51.272 GCD[11750:1914722] —-barrier—–<NSThread: 0x7fb182423fd0>{number = 3, name = (null)}
2016-09-03 19:35:51.273 GCD[11750:1914722] —-3—–<NSThread: 0x7fb182423fd0>{number = 3, name = (null)}
2016-09-03 19:35:51.273 GCD[11750:1914724] —-4—–<NSThread: 0x7fb1826047b0>{number = 2, name = (null)}
- 可以看出在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
2、GCD延时执行方法 dispatch_after
- 当我们需要延迟执行一段代码时,就需要用到GCD的
dispatch_after
方法
1 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ |
3、GCD的一次性代码(只执行一次)dispatch_once
*我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了GCD的dispatch_once
方法。使用dispatch_once
函数能保证某段代码在程序运行过程中只执行1次。
1 | static dispatch_once_t onceToken; |
4、GCD的快速迭代方法 dispatch_apply
- 通常我们会for循环遍历,但是GCD给我们提供了快速迭代的方法
dispatch_apply
,使我们可以同时遍历。dispatch_apply
可以同时遍历多个数字。
1 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
输出结果:
2016-09-03 19:37:02.250 GCD[11764:1915764] 1——<NSThread: 0x7fac9a7029e0>{number = 1, name = main}
2016-09-03 19:37:02.250 GCD[11764:1915885] 0——<NSThread: 0x7fac9a614bd0>{number = 2, name = (null)}
2016-09-03 19:37:02.250 GCD[11764:1915886] 2——<NSThread: 0x7fac9a542b20>{number = 3, name = (null)}
2016-09-03 19:37:02.251 GCD[11764:1915764] 4——<NSThread: 0x7fac9a7029e0>{number = 1, name = main}
2016-09-03 19:37:02.250 GCD[11764:1915884] 3——<NSThread: 0x7fac9a76ca10>{number = 4, name = (null)}
2016-09-03 19:37:02.251 GCD[11764:1915885] 5——<NSThread: 0x7fac9a614bd0>{number = 2, name = (null)}
从输出结果中前边的时间中可以看出,几乎是同时便利的。
5、GCD的队列组 dispatch_group
*有时候我们会有这样的需求:分别异步执行2个耗时操作,然后当2个耗时操作都执行完毕后再回到主线程执行操作。这时候我们可以用到GCD的队列组。
- 我们可以先把任务放到队列中,然后将队列放入队列组中
- 调用队列组的
dispatch_group_notify
回到主线程执行操作。
1 | dispatch_group_t group = dispatch_group_create(); |
6、GCD dispatch_semaphore 信号量
在GCD中提供了一种信号机制,也可以解决资源抢占问题(和同步锁的机制并不一样)。
GCD中信号量是dispatch_semaphore_t
类型,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1,每当发送一个等待信号时信号量-1。如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。根据这个原理我们可以初始化一个信号量变量,默认信号量设为1,每当有线程进入“加锁代码”之后就调用信号等待命令(此时信号量为0)开始等待,此时其他线程无法加入,执行完毕之后发送信号通知(此时信号量为1),其他线程开始进入执行,如此就达到了线程同步的目的。
1 | dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
上述代码中用到的三个重要函数的具体介绍:
dispatch_semaphore_create()
创建信号量。传入的参数为long型,且必须大于或者等于0,否则函数返回NULL。dispatch_semaphore_wait()
方法为信号等待。如果信号量值为0,那么这个函数就阻塞当前线程等待timeOutCount,如果等待期间信号量大于0,则开始执行“加锁代码”,同时会使信号量-1。这个方法的返回值是当成功时则返回0,超时失败时则返回非0值。dispatch_semaphore_signal()
方法会使信号量+1。返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一个等待的线程(当线程有优先级时,唤醒优先级最高的线程,否则随机唤醒)。
NSOperation
NSOperation简介
NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD更高一层的封装,但是比GCD更简单易用,代码可读性也高。
NSOperation需要配合NSOperationQueue来实现多线程。因为默认情况下,NSOperation单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue才能实现异步操作。
因为NSOperation是基于GCD的,那么使用起来也和GCD差不多,其中,NSOperation相当于GCD中的任务,而NSOperationQueue则相当于GCD中的队列。NSOperation实现多线程的使用步骤分为三步:
- 创建任务:现将需要执行的操作封装到一个NSOperation对象中。
- 创建队列:创建NSOperationQueue对象。
- 将任务加到队列中:然后将NSOperation对象添加到NSOperationQueue中。
之后呢,系统就会自动将NSOperationQueue中的NSOperation取出来,在新线程中执行操作。
下面我们来看看NSOperation和NSOperationQueue的基本使用。
NSOperation和NSOperationQueue的基本使用
1、创建任务
NSOperation是个抽象类,并不能封装任务。我们只有使用它的子类来封装任务。我们有三种方式来封装任务。
- 使用子类NSInvocationOperation
- 使用子类NSBlockOperation
- 定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务。
在不使用NSOperationQueue,单独使用NSOperation的情况下系统同步执行操作,下面我们看看以下任务的三种创建方式。
(1)、使用子类- NSInvocationOperation:
1 | //1、创建NSInvocationOperation对象 |
输出结果:
2016-09-05 14:29:58.483 NSOperation[15834:2384555] ——<NSThread: 0x7fa3e2e05410>{number = 1, name = main}
从中可以看到,在没有使用NSOperationQueue、单独使用NSInvocationOperation的情况下,NSInvocationOperation在主线程操作,并没有开启新线程。
(2)、使用子类- NSBlockOperation
1 | NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ |
输出结果:
2016-09-05 14:33:15.268 NSOperation[15884:2387780] ——<NSThread: 0x7fb2196012c0>{number = 1, name = main}
我们同样可以看到,在没有使用NSOperationQueue、单独使用NSBlockOperation的情况下,NSBlockOperation也是在主线程执行操作,并没有开启新线程。
但是,NSBlockOperation还提供了一个方法addExecutionBlock:
,通过addExecutionBlock:
就可以为NSBlockOperation添加额外的操作,这些额外的操作就会在其他线程并发执行。
1 | - (void)blockOperation { |
输出结果:
2016-09-05 14:36:59.353 NSOperation[15896:2390616] 1——<NSThread: 0x7ff633f03be0>{number = 1, name = main}
2016-09-05 14:36:59.354 NSOperation[15896:2390825] 2——<NSThread: 0x7ff633e24600>{number = 2, name = (null)}
2016-09-05 14:36:59.354 NSOperation[15896:2390657] 3——<NSThread: 0x7ff633c411e0>{number = 3, name = (null)}
2016-09-05 14:36:59.354 NSOperation[15896:2390656] 4——<NSThread: 0x7ff633f1d3e0>{number = 4, name = (null)}
可以看出,blockOperationWithBlock:
方法中的操作是在主线程中执行的,而addExecutionBlock:
方法中的操作是在其他线程中执行的。
(3)、定义继承自NSOperation的子类
先定义一个继承自NSOperation的子类,重写main方法
1 | #import <Foundation/Foundation.h> |
然后使用的时候导入头文件YSCOperation.h
1 | //创建YSCOperation |
输出结果:
2016-09-05 18:15:59.674 NSOperation[16566:2501606] 1—–<NSThread: 0x7f8030d05150>{number = 1, name = main}
2016-09-05 18:15:59.675 NSOperation[16566:2501606] 1—–<NSThread: 0x7f8030d05150>{number = 1, name = main}
可以看出:在没有使用NSOperationQueue、单独使用自定义子类的情况下,是在主线程执行操作、并没有开启新线程。
下边我们简单讲讲NSOperationQueue的创建
2、创建队列
和GCD中的并发队列、串行队列略有不同的是NSOperationQueue
一共有两种队列:主队列、其他队列。其中其他队列包含了串行、并发功能。下面是主队列、其他队列的基本创建方法和特点。
- 主队列
凡是添加到主队列中的任务(NSOperation),都会放倒主线程中执行
1 | NSOperationQueue *queue = [NSOperationQueue mainQueue]; |
- 其他队列(非主队列)
添加到这种队列中的任务(NSOperation),就会自动放到子线程中执行;
同时包含了:串行、并发功能
1 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; |
3、将任务加入到队列中
前边说了,NSOperation需要配合NSOperationQueue来实现多线程。那么我们需要将创建好的任务加入到队列中去。总共有两种方法:
- (void)addOperation:(NSOperation *)op;
需要先创建任务,再将创建好的任务加入到创建好的队列中去
1 | - (void)addOperation:(NSOperation *)op { |
输出结果:
2016-09-05 17:06:00.241 NSOperationQueue[16201:2452281] 1—–<NSThread: 0x7fe4824080e0>{number = 3, name = (null)}
2016-09-05 17:06:00.241 NSOperationQueue[16201:2452175] 2—–<NSThread: 0x7fe482404a50>{number = 2, name = (null)}
2016-09-05 17:06:00.242 NSOperationQueue[16201:2452175] 2—–<NSThread: 0x7fe482404a50>{number = 2, name = (null)}
2016-09-05 17:06:00.241 NSOperationQueue[16201:2452281] 1—–<NSThread: 0x7fe4824080e0>{number = 3, name = (null)}
可以看出:NSInvocationOperation和NSOperationQueue结合后能够开启新线程,进行并发执行NSBlockOperation和NSOperationQueue也能够开启新线程,进行并发执行。
2.- (void)addOperationWithBlock:(void (^)(void))block
;
无需先创建任务,在block中添加任务,直接将任务block加入到队列中。
1 | - (void)addOperationWithBlockToQueue { |
输出结果:
2016-09-05 17:10:47.023 NSOperationQueue[16293:2457487] —–<NSThread: 0x7ffa6bc0e1e0>{number = 2, name = (null)}
2016-09-05 17:10:47.024 NSOperationQueue[16293:2457487] —–<NSThread: 0x7ffa6bc0e1e0>{number = 2, name = (null)}
可以看出addOperationWithBlock:和NSOperationQueue能够开启新线程,进行并发执行。
4、控制串行执行和并发执行的关键
之前我们说过,NSOperationQueue创建的其他队列同时具有串行、并发功能,上边我们演示了并发功能,那么他的串行功能是如何实现的?
这里有个关键参数maxConcurrentOperationCount
,叫做最大并发数。
最大并发数:maxConcurrentOperationCount
maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,默认并发执行。
当maxConcurrentOperationCount
为1时,进行串行执行。
当maxConcurrentOperationCount
大于1时,进行并发执行,当然这个值不应该超过系统限制,即使自己设置一个很大的值,系统也会自动调整。
1 | - (void)operationQueue { |
最大并发数为1输出结果:
2016-09-05 17:21:54.124 NSOperationQueue[16320:2464630] 1—–<NSThread: 0x7fc892d0b3a0>{number = 2, name = (null)}
2016-09-05 17:21:54.136 NSOperationQueue[16320:2464631] 2—–<NSThread: 0x7fc892c0a7b0>{number = 3, name = (null)}
2016-09-05 17:21:54.148 NSOperationQueue[16320:2464630] 3—–<NSThread: 0x7fc892d0b3a0>{number = 2, name = (null)}
2016-09-05 17:21:54.160 NSOperationQueue[16320:2464631] 4—–<NSThread: 0x7fc892c0a7b0>{number = 3, name = (null)}
2016-09-05 17:21:54.171 NSOperationQueue[16320:2464631] 5—–<NSThread: 0x7fc892c0a7b0>{number = 3, name = (null)}
2016-09-05 17:21:54.184 NSOperationQueue[16320:2464630] 6—–<NSThread: 0x7fc892d0b3a0>{number = 2, name = (null)}
最大并发数为2输出结果:
2016-09-05 17:23:36.030 NSOperationQueue[16331:2466366] 2—–<NSThread: 0x7fd729f0f270>{number = 3, name = (null)}
2016-09-05 17:23:36.030 NSOperationQueue[16331:2466491] 1—–<NSThread: 0x7fd729f4e290>{number = 2, name = (null)}
2016-09-05 17:23:36.041 NSOperationQueue[16331:2466367] 3—–<NSThread: 0x7fd729d214e0>{number = 4, name = (null)}
2016-09-05 17:23:36.041 NSOperationQueue[16331:2466366] 4—–<NSThread: 0x7fd729f0f270>{number = 3, name = (null)}
2016-09-05 17:23:36.053 NSOperationQueue[16331:2466366] 6—–<NSThread: 0x7fd729f0f270>{number = 3, name = (null)}
2016-09-05 17:23:36.053 NSOperationQueue[16331:2466511] 5—–<NSThread: 0x7fd729e056c0>{number = 5, name = (null)}
5、操作依赖
NSOperation和NSOperationQueue最吸引人的地方是它能添加操作之间的依赖关系。比如说有A、B两个操作,其中A执行完操作,B才能执行操作,那么就需要让B依赖于A。具体如下:
1 | - (void)addDependency { |
输出结果:
2016-09-05 17:51:28.811 操作依赖[16423:2484866] 1—–<NSThread: 0x7fc138e1e7c0>{number = 2, name = (null)}
2016-09-05 17:51:28.812 操作依赖[16423:2484866] 2—–<NSThread: 0x7fc138e1e7c0>{number = 2, name = (null)}
可以看到,无论运行几次,其结果都是op1先执行,op2后执行。
6、一些其他方法
- (void)cancel;
NSOperation提供的方法,可取消单个操作- (void)cancelAllOperations;
NSOperationQueue提供的方法,可以取消队列的所有操作- (void)setSuspended:(BOOL)b;
可设置任务的暂停和恢复,YES代表暂停队列,NO代表恢复队列- (BOOL)isSuspended;
判断暂停状态
注意:
这里的暂停和取消并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
暂停和取消的区别在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。