CoderHann

iOS多线程的使用总结

多线程简介

进程:进程是描述一个程序的执行过程,是应用程序的一个实例(一个应用程序或软件可以有多个进程)。它是操作系统分配资源的基本单元,即拥有自己的地址空间、存储空间。
线程:线程是操作系统独立运行和独立调度的基本单位,可以理解为程序执行过程中的子过程,这个子过程可是并行也可串行(一个进程至少有一个线程)。
多线程:多线程是一种用来提高CPU运行效率的一种技术,往往提现为一个进程中分配了不止一条线程在执行任务,而是多条线程并发执行。

上面以自己的理解对进程线程多线程概念的概括,需要详细了解相关知识的请自行Google、百度进行查阅。本文主要是以自己对多线程的理解进行梳理,如果有不对的地方请及时联系我更正。

生活中的多线程

其实多线程在我们的生活中有很多,如果说做饭是一个进程的话。我们把这个进程分成煲汤和煮面这两个线程(排骨汤配面条绝了😝),不会说把它们放到一个线程中依次进行,而是两个线程并发去完成,这样可以提高我们的执行效率。

上面的一个生活中的小例子只是概括的想象下程序的多线程是什么样子的,下面这个例子主要体现在多线程的处理效率上的提升:

上学的时候,同学们上完上午的课之后一起涌进食堂。如果食堂只有一个窗口,吃饭的人多了肯定会有很多人排队吃饭,这样的效率应该是最低的。食堂改革了新增了N个窗口提供用餐,我们可以将这种多窗口的改革称之为多线程的使用。这种方式能够解决执行效率低下的情况,排队时间缩短了M倍!除此之外,一个学校可能不止有一个食堂可能有第二、第三食堂,我们可以把这种情况看做是多核心处理。多核处理能让我们的执行效率得到更一步的提升!

上面叙述的目的就是让大家对多线程有一个直观的认识以及多线程处理性能高的优点。下面让我们了解下iOS开发中常用的多线程相关技术吧。

iOS多线程的分类以及详解

概述

iOS开发中提供了pthreadNSThreadGCDNSOperation这四种方式来使用多线程。其中GCDNSOperation使用频率比较高,将花费本博客的一大部分篇幅。而pthread在普通日常开发中很少遇到,这里将仅仅介绍相关的简单使用方法。NSThread在开发中用的频率也比较低,它是以面向对象的方式来处理多线程的。下面我们分别就这四种方式进行详细的介绍。

pthread

pthread由于开发中很少使用(基本没用过),所以这里只是对它做一个简单介绍,然后使用它调起多线程。

pthread的介绍参考百科如下:
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32。

使用pthread开启多线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1.导入头文件
#import <pthread.h>
- (void)aMethod {
// 2.创建线程变量
pthread_t pthread;
// 3.1创建线程
static long times = 10000L;
pthread_create(&pthread, NULL, run, &times);
}
// 3.2线程指定调用c函数(耗时操作)
void* run(void* times) {
long executeCount = *(long *)times;
for (int index = 0; index < executeCount; index++) {
NSLog(@"耗时任务正在执行:%d times on thread:%@",index,[NSThread currentThread]);
}
return NULL;
}

pthread_create()有四个参数:
1.pthread_t *:指向线程变量的指针
2.const pthread_attr_t *:指向线程属性的常量指针
3.void * (*)(void *):指向返回值为void *,入参为void *的c函数(线程要执行的任务)
4.void *:指向任务函数的入参指针

上面代码模拟了在开发中不影响主线程的情况下,开启子线程处理耗时操作(对应这里的打印10000次log)。其实这里还有很多问题,比如我们创建线程执行完任务后没有将该线程释放。我们可以在子线程任务结束的时候调用pthread_detach(pthread_self());让线程运行结束后自动释放资源。

pthread这种方式使用c语言,以及需要程序猿自己管理线程的生命周期,大大降低了我们的开发速率。日常普通开发不推荐使用此方式,如果你想了解更多pthread相关知识请百度或谷歌寻找答案!

NSThread

NSThread在开发中还是比较常见的,起码[NSThread currentThread]大家都用过。相比于pthread的简单介绍,这里我们将通过NSThread基础用法线程的生命周期线程间的通讯以及一个线程同步对NSThread进行详细讲解。

NSThread基础用法
NSThread的实例化

NSThread的实例对象作为一个线程的实例,我们所有的操作都是基于这个实例对象,那这样的实例对象该如何创建呢:
①使用类方法创建线程:

1
2
3
4
5
6
7
8
9
/** 该方法创建一个新的线程并执行selector对应的方法体
* selector: 对应要执行的方法体
* target: 执行方法的对象
* argument: 给target的参数
*/
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
// iOS10新增了使用block对应的方法,block内为要在新线程执行的代码/任务
+ (void)detachNewThreadWithBlock:(void (^)(void))block;

类方法创建的线程都没有返回对应的线程对象,如果要对该线程对象进行操作可以在对应的方法体中通过[NSThread currentThread]获取当前线程。

②使用对象方法创建线程:

1
2
3
4
// 创建一个线程对象并返回`AThread`
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
// iOS10新增的对象方法,它接受一个block用来处理要执行的代码/任务
- (instancetype)initWithBlock:(void (^)(void))block;

使用②中的对象方法创建的线程不会立即执行,需要对创建的线程对象调用start方法:[aThread start]

③使用NSObject的分类方法创建线程:

1
2
3
4
5
6
7
8
9
@interface NSObject (NSThreadPerformAdditions)
/** 该方法将创建一个线程,并执行aSelector对应的方法体。
* aSelector: 对应调用此方法对象的一个方法体
* arg: 给aSelector选择器传递的参数
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
@end

NSThread相关的实例化方法正如上面的三种分类所示,了解了创建线程实例对象后,我们要深入线程对象从对象相关的属性、用法来介绍。

NSThread常用属性&方法

该小节主要介绍官方提供的我们经常使用的那些类属性、类方法、对象属性、对象方法,来了解NSThread都有哪些功能。
常用的类属性&方法:

1
2
3
4
5
6
7
8
// 获得当前执行任务所在的线程
@property (class, readonly, strong) NSThread *currentThread;
// 判断当前线程是否是主线程
@property (class, readonly) BOOL isMainThread;
// 获得主线程
@property (class, readonly, strong) NSThread *mainThread;

1
2
3
4
5
6
// 线程暂停
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 线程终止,调用此方法会将所有非主线程的线程杀掉
+ (void)exit;

常用的对象属性&方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 当前线程名字
@property (nullable, copy) NSString *name
/** 线程优先级枚举,需在线程开始前设置否则无效,枚举值如下、优先级依次降低:
* NSQualityOfServiceUserInteractive: 用于用户交互事件以及UI的刷新
* NSQualityOfServiceUserInitiated: 用于用户需要进一步执行的事件(在用户在邮件列表中选择邮件后,加载电子邮件。)
* NSQualityOfServiceDefault: 默认分配的优先级(根据任务来源推断优先级,如无法推断,优先级介于UserInitiated和Utility之间)
* NSQualityOfServiceUtility: 用于执行用户不太可能立即等待结果的工作(周期性内容更新或大容量文件操作,如媒体导入.)
* NSQualityOfServiceBackground: 用于非用户发起或可见的工作(预取内容,搜索索引、备份和同步与外部系统的数据。)
*/
@property NSQualityOfService qualityOfService;
// 判断当前线程是否是主线程
@property (readonly) BOOL isMainThread;
// 线程状态:是否正在执行,是否已经完成,是否已经取消
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;

1
2
3
4
5
// 线程开始执行
- (void)start;
// 取消线程
- (void)cancel;

至此NSThread的实例创建以及基本功能大家都了解了,至少能使用NSThread开启一个线程做任务了。下面将对线程相关进行扩展,讲述线程的生命周期是怎么在NSThread中体现的。

线程的生命周期

像很多的实例对象一样,线程也有自己的生命周期。线程的生命周期简单的来说就是线程的创建到线程的销毁的过程。本小节将围绕这个过程存在的状态:创建就绪运行阻塞销毁进行讲解。

创建:创建一个线程对象,在调用start方法之前的状态。

1
2
// 创建:aThread被创建
NSThread *aThread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];

就绪:就绪状态在代码中的常见形式是调用了start方法即:[aThread start],该状态描述的是线程对象已加入就绪队列中并等待CPU的资源。就绪状态也可描述为可运行状态,是运行状态的前一个必有的状态。该状态除了调用start方法还有很多其它情况,比如线程睡眠结束、共享资源锁的释放,IO操作完成等。

运行:当就绪的线程aThread得到CPU的资源时就会将线程对象激活进入运行状态,处理代码逻辑。

阻塞:阻塞是在线程运行中由于某种原因失去CPU资源的结果。常见的这些原因主要有:线程睡眠,IO操作,等待线程/锁。

1
2
3
4
5
6
// 阻塞:线程暂停3秒
- (void)downloadImage {
// xxx代码
[NSThread sleepForTimeInterval:3];
// xxx代码
}

销毁:销毁即线程的死亡状态,通常是运行完成后自动销毁,或者在运行期间调用了exit方法导致线程被结束。

线程的生命周期作为扩展小节,目的是让大家更好的理解线程是如何工作的,也为我们以后的开发能更好的处理问题提供线索。接下来讲述平常开发中常见的情景:线程间的通讯,来了解下各个线程如何沟通或者传递数据。

NSThread线程间的通讯

前面我们也说到了一个进程可以有多条线程在执行任务,这些线程之间的关系可能是相互独立的各自完成相应的任务,也有依赖关系的通常表现在一个线程所产生的数据,消息等通知给另一个线程。比如:用线程A去加载一个比较大的图片,加载完成后需要告诉主线程将图片渲染在屏幕上,我们把这种线程中的数据交流称为线程间的通讯

NSThread线程通讯的常用方式:
在NSObject的NSThreadPerformAdditions分类中提供了从一个线程调起另一个线程的方法,下面我们将围绕这几个方法进行介绍,最后以一个例子结束线程通讯模块。

1
2
3
4
5
6
7
8
/** 到thr线程执行aSelector选择器指定的方法
* aSelector: 一个选择器,对应要执行的代码方法
* thr: 指定的线程对象
* arg: aSelector所需要接受收的参数
* wait: 是否阻塞当前线程,直到指定线程thr执行完aSelector对应的方法后再执行下面其它代码。
* array: 循环队列(run loop)参数数组kCFRunLoopDefaultMode,kCFRunLoopCommonModes
*/
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;

1
2
// 该方法是上一个方法array参数为kCFRunLoopCommonModes的情况,具体参数参照如上
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
1
2
// 到主线程执行aSelector选择器指定的方法,参数可参考第一个方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
1
2
// array参数为kCFRunLoopCommonModes的情况
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

下面代码主要演示主线程绘画一个UIImageView,而它的图片来自于互联网需要开启一个异步线程去下载,当图片下载好之后需要主线程将图片再渲染到屏幕上,这样来模拟线程间的通讯的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (void)viewDidLoad {
[super viewDidLoad];
// 绘画imageView
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(100, 100, 100, 100);
imageView.backgroundColor = [UIColor grayColor];
[self.view addSubview:imageView];
_imageView = imageView;
// 开启线程开启下载任务
NSThread *aThread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];
[aThread start];
}
// 子线程下载图片方法
- (void)downloadImage {
NSURL *url = [NSURL URLWithString:@"https://raw.githubusercontent.com/CoderHann/imageSource/master/Blog/blog_usr_icon.jpeg"];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];
NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 网络访问成功
if (data && (error == nil)) {
UIImage *image = [UIImage imageWithData:data];
[self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:NO];
}
}];
[dataTask resume];
}
// 更新_imageView图片
- (void)updateImage:(UIImage *)image {
_imageView.image = image;
}

这里注意下dataTaskWithRequest: completionHandler:这个方法的block代码也是异步调用的,虽然不在我们指定的线程中通知主线程进行刷新UI,但是不影响我们进行线程间通信的演示。

NSThread线程同步

多线程环境下存在同时访问共享数据的情况,如果不对数据的访问进行处理/限制极有可能造成数据混乱或是数据处理异常。这里我们引用经典的买票案例演示多线程访问存在的问题,以及解决办法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 卖票相关初始化
- (void)prepareForSale {
// 北京窗口
NSThread *beijing = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
beijing.name = @"北京";
// 上海窗口
NSThread *shanghai = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
shanghai.name = @"上海";
// 监听线程的退出,判断卖票是否结束
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadWillExit) name:NSThreadWillExitNotification object:nil];
_leftTicketNum = 10;
// 开始售票
[beijing start];
[shanghai start];
}
// 售票
- (void)saleTickets {
while (_leftTicketNum > 0) {
_leftTicketNum--;
NSThread *thread = [NSThread currentThread];
NSLog(@"%@卖出一张票,剩余%zd张",thread.name,_leftTicketNum);
[NSThread sleepForTimeInterval:0.2];
} else {
break;
}
}
// 卖票结束即线程销毁
- (void)threadWillExit {
NSLog(@"%@卖票结束",[NSThread currentThread].name);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2017-04-16 16:28:11.297 MultiThreadDemo[3564:228402] 上海卖出一张票,剩余8
2017-04-16 16:28:11.297 MultiThreadDemo[3564:228401] 北京卖出一张票,剩余9
2017-04-16 16:28:11.499 MultiThreadDemo[3564:228401] 北京卖出一张票,剩余7
2017-04-16 16:28:11.499 MultiThreadDemo[3564:228402] 上海卖出一张票,剩余7
2017-04-16 16:28:11.702 MultiThreadDemo[3564:228401] 北京卖出一张票,剩余6
2017-04-16 16:28:11.702 MultiThreadDemo[3564:228402] 上海卖出一张票,剩余5
2017-04-16 16:28:11.905 MultiThreadDemo[3564:228402] 上海卖出一张票,剩余3
2017-04-16 16:28:11.905 MultiThreadDemo[3564:228401] 北京卖出一张票,剩余4
2017-04-16 16:28:12.110 MultiThreadDemo[3564:228401] 北京卖出一张票,剩余2
2017-04-16 16:28:12.110 MultiThreadDemo[3564:228402] 上海卖出一张票,剩余2
2017-04-16 16:28:12.312 MultiThreadDemo[3564:228401] 北京卖出一张票,剩余1
2017-04-16 16:28:12.312 MultiThreadDemo[3564:228402] 上海卖出一张票,剩余0
2017-04-16 16:28:12.517 MultiThreadDemo[3564:228402] 上海卖票结束
2017-04-16 16:28:12.517 MultiThreadDemo[3564:228401] 北京卖票结束

上面代码演示了北京和上海两地同时卖票的情况,结果打印的数据存在着问题,而造成这种问题的原因是线程间共享数据的读和存不同步。比如上海读取票数为10,在上海修改票数之前北京读取也为10,这样两个窗口卖了两张票之后总票数还为9。通常处理这种问题的方案是给这个读存操作加锁,即在一个窗口A访问数据时其他窗口不能访问该数据直到A访问完成。

解决方案:对售票saleTickets业务访问共享数据的逻辑加锁操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 售票
- (void)saleTickets {
while (1) {
@synchronized (self) {
if (_leftTicketNum > 0) {
_leftTicketNum--;
NSThread *thread = [NSThread currentThread];
NSLog(@"%@卖出一张票,剩余%zd张",thread.name,_leftTicketNum);
[NSThread sleepForTimeInterval:0.2];
} else {
break;
}
}
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
2017-04-16 16:35:29.410 MultiThreadDemo[744:14961] 北京卖出一张票,剩余9
2017-04-16 16:35:29.410 MultiThreadDemo[744:14962] 上海卖出一张票,剩余8
2017-04-16 16:35:29.613 MultiThreadDemo[744:14961] 北京卖出一张票,剩余7
2017-04-16 16:35:29.614 MultiThreadDemo[744:14962] 上海卖出一张票,剩余6
2017-04-16 16:35:29.817 MultiThreadDemo[744:14962] 上海卖出一张票,剩余5
2017-04-16 16:35:29.818 MultiThreadDemo[744:14961] 北京卖出一张票,剩余4
2017-04-16 16:35:30.019 MultiThreadDemo[744:14961] 北京卖出一张票,剩余3
2017-04-16 16:35:30.019 MultiThreadDemo[744:14962] 上海卖出一张票,剩余2
2017-04-16 16:35:30.223 MultiThreadDemo[744:14961] 北京卖出一张票,剩余1
2017-04-16 16:35:30.224 MultiThreadDemo[744:14962] 上海卖出一张票,剩余0
2017-04-16 16:35:30.429 MultiThreadDemo[744:14962] 上海卖票结束
2017-04-16 16:35:30.429 MultiThreadDemo[744:14961] 北京卖票结束

可以看出对共享数据访问加锁的方式成功解决了数据混乱的问题。在iOS开发中加锁的方式不只是使用@synchronized(){}还有其他类型的锁这里不一一介绍,有兴趣的童鞋可以搜搜相关博客介绍。

到此NSThread的相关知识点从基本介绍到多线程数据的访问限制已经比较全面的讲述了一番,因为NSThread的轻型以及面向对象开发方式,总体来看使用NSThread操作多线程是还比较容易上手的。在其优点的对面当然也有它的不足:我们需要管理线程的生命周期,对共享数据加锁开销比较大。NSThread相关介绍到此结束,下面让我们看看开发中使用频率非常高的GCD吧!

GCD

GCD是Grand Central Dispatch的缩写,由苹果公司开发的优化多核处理器的技术。它的基本思想是将线程的管理从开发中分离出去,让开发者更专注于自己的业务开发。在日常开发中经常使用GCD处理多线程相关业务,所以学会使用GCD是非常重要的。按照NSThread的套路我们仍然从基础开始由浅入深的对GCD进行讲解,力求大家看完GCD章节内容后能熟练的使用它。本节将通过:GCD相关概念常用APIGCD的基本使用线程间通信线程安全对GCD进行详细讲解。

GCD相关概念

因为GCD执行的方式是将任务放入队列中进行同步/异步执行,所以在使用GCD之前,我们先对任务队列这两个核心概念进行了解。

任务:简单的说我们要执行的代码片段要完成的业务就是一个任务,而开发中这些任务一般是处理一些耗时的操作。通常任务都被描述为被执行的,在GCD中执行任务分为两种形式:同步执行异步执行。同步执行不具备开启新线程的能力,就在原线程中执行任务。而异步执行是具备开启新线程的能力的,但是开不开、开几条线程是跟队列相关的,后面我们会在GCD的基本使用小节中详细介绍他们的用法。

队列:这里的队列是用来存放任务的,也称为任务队列。GCD中分为串行队列并行队列两类。串行队列其特点是一个任务执行完了才执行下一个任务遵循队列的先进先出原则,而并行队列在异步执行的情况下可将各个任务依照先进先出原则并发执行。

概念补充:
并发执行:并发描述为多个事件在同一时间间隔内发生。由于CPU的处理速度非常快,CPU完全有能力在短暂的时间间隔内完成多条线程的切换以及各个线程部分任务的执行。并发能够有效提升CPU的执行效率。

并行执行:并行描述为多个事件在同一时间点发生。并行执行发生在拥有多核处理器的设备上,在同一时间点不同的处理器执行不同的线程不同的任务。并行的多核处理器仿佛拥有多个心脏/引擎一样拥有更强大的’动力’,结合并发执行能够显著提升任务的执行率。

GCD基础

在了解了基本的概念之后我们主要通过队列同步异步执行来了解GCD的使用。

dispatch_queue队列

队列在GCD中的类型为dispatch_queue_t,本小节将围绕队列的创建、使用和特性进行讲解。队列除了按照性质分类的串行和并行队列,我们还可以将队列分为主队列全局队列自定义队列下面让我们以这三种分类对队列进行介绍。

主队列:即主线程队列,在我们的开发中主线程即UI线程。主队列为串行队列,在开发中无法自定义创建,其获取方式可以使用官方提供的APIdispatch_get_main_queue()获取如下:

1
2
// 主队列的获取方式
dispatch_queue_t mainQueue = dispatch_get_main_queue();

全局队列:是系统提供方便编程的一个并行队列,其获取方式使用dispatch_get_global_queue(long identifier, unsigned long flags)。其中有两个参数identifierflags分别表示队列的优先级以及预留参数。
identifier优先级分了四个等级,在NSThread章节讲过创建的线程我们也可以给它设置优先级,那么这两块的优先级有什么联系呢,下面针对GCD全局队列的四个等级进行映射:

1
2
3
4
DISPATCH_QUEUE_PRIORITY_HIGH: NSQualityOfServiceUserInitiated
DISPATCH_QUEUE_PRIORITY_DEFAULT: NSQualityOfServiceDefault
DISPATCH_QUEUE_PRIORITY_LOW: NSQualityOfServiceUtility
DISPATCH_QUEUE_PRIORITY_BACKGROUND: NSQualityOfServiceBackground

对应的优先级关系可参考NSThread线程优先级介绍,由于全局队列处理一些耗时/后台的一些操作,其优先级并没有提供与NSQualityOfServiceUserInteractive对应的刷新UI的优先级。

flags预留参数:该参数作为预留参数现阶段开发中并没有什么用,但是官方提到Passing any value other than zero may result in a NULL return value.大概意思就是如果此处传参不为0可能返回一个NULL的队列,所以开发中这个参数我们都写0即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 全局队列的创建
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t highQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t lowQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_queue_t backgroundQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
NSLog(@"highQueue:%p",highQueue);
NSLog(@"highQueue2:%p",highQueue2);
NSLog(@"defaultQueue:%p",defaultQueue);
NSLog(@"lowQueue:%p",lowQueue);
NSLog(@"backgroundQueue:%p",backgroundQueue);
NSLog(@"backgroundQueue2:%p",backgroundQueue2);
// 对应的log
2017-04-17 11:00:31.952 MultiThreadDemo[1552:107092] highQueue:0x10e3ea540
2017-04-17 11:00:31.953 MultiThreadDemo[1552:107092] highQueue2:0x10e3ea540
2017-04-17 11:00:31.953 MultiThreadDemo[1552:107092] defaultQueue:0x10e3ea3c0
2017-04-17 11:00:31.953 MultiThreadDemo[1552:107092] lowQueue:0x10e3ea240
2017-04-17 11:00:31.953 MultiThreadDemo[1552:107092] backgroundQueue:0x10e3ea0c0
2017-04-17 11:00:31.953 MultiThreadDemo[1552:107092] backgroundQueue2:0x10e3ea0c0

根据打印的log我们发现了一个问题,就是相同优先级获取到的队列是同一个队列,而不同优先级获取的队列地址是不同的。也就是说全局队列不止一个队列对象是四种不同优先级的四个全局队列组成的。

自定义队列:除了使用系统提供的主队列和全局队列,我们还可以根据自己的需求创建队列,自定义队列也是分串行队列和并行队列的,下面让我们看看自定义队列如何创建:

1
2
3
4
5
// 创建串行队列DISPATCH_QUEUE_SERIAL
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
// 创建并行队列DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create(const char *label,dispatch_queue_attr_t attr)方法提供了两个入参:label为线程唯一标示方便于debug找问题,attr是队列属性用于控制创建的队列是串行(DISPATCH_QUEUE_SERIAL)还是并行(DISPATCH_QUEUE_CONCURRENT)的。

sync同步&async异步执行

到现在我们已经知道如何创建不同类型的队列,那么我们如何执行队列中的任务呢。GCD中提供了同步执行和异步执行任务,同步和异步执行的特点在GCD概念介绍的时候已经讲过这里不再赘述。下面让我看看两种方式的使用吧:

同步执行:dispatch_sync(dispatch_queue_t queue, ^(void)block)同步执行方法接收两个参数,queue指定要在哪个任务队列中执行,block所指代码块为要执行的任务。

1
2
3
dispatch_sync(defaultQueue, ^{
// 任务代码
});

异步执行:dispatch_async(dispatch_queue_t queue, ^(void)block)和同步执行方法类似也拥有相同的入参,请参考同步执行入参介绍。

1
2
3
dispatch_async(defaultQueue, ^{
// 任务代码
});

下面将结合串行并行队列诠释同步与异步执行的不同:
同步+串行队列:不会开启新线程,任务遵循先进先出方式依次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)viewDidLoad {
[super viewDidLoad];
// 串行队列创建
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"task1--%@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"task2--%@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"task3--%@",[NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"task4--%@",[NSThread currentThread]);
});
}
// 对应的log
2017-04-17 15:28:56.691 MultiThreadDemo[16842:220632] task1--<NSThread: 0x60800006b940>{number = 1, name = main}
2017-04-17 15:28:56.692 MultiThreadDemo[16842:220632] task2--<NSThread: 0x60800006b940>{number = 1, name = main}
2017-04-17 15:28:56.692 MultiThreadDemo[16842:220632] task3--<NSThread: 0x60800006b940>{number = 1, name = main}
2017-04-17 15:28:56.692 MultiThreadDemo[16842:220632] task4--<NSThread: 0x60800006b940>{number = 1, name = main}

前面讲队列的时候我们说了主队列也是属于串行队列的,如果同步执行相同队列中的任务会是什么样的情况呢,下面展示主线程中在主队列中执行任务:

1
2
3
4
5
6
7
8
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
NSLog(@"task1--%@",[NSThread currentThread]);
});
}

运行的结果是程序crash了EXC_BAD_INSTRUCTION(code=EXC_I386_INVOP,subcode=0x0),因为同步串行执行需等前一个任务有返回值或者说结束了才会去执行下一个任务,示例中添加的task1在串行队列的最后面,前一个任务没有执行完要接着执行,而此时又要立即执行task1,出现了任务互等相互死锁都执行不了。

主队列是一个比较特殊的串行队列,那是不是一般的串行队列也有这样的问题呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
// 串行队列创建
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"task1 start");
NSLog(@"task1--%@",[NSThread currentThread]);
dispatch_sync(serialQueue, ^{
NSLog(@"task2--%@",[NSThread currentThread]);
});
NSLog(@"task1 end");
});
}
// 打印的log
2017-04-17 22:27:37.243 MultiThreadDemo[808:37406] task1 start
2017-04-17 22:27:39.606 MultiThreadDemo[808:37406] task1--<NSThread: 0x60000007a600>{number = 3, name = (null)}

这里发生了相同的crash,也就是说同步执行相同串行队列的任务时会发生死锁,所以在多线程开发中不要走这种雷区。不过这种情况一般人也不会这样写,直接插入对应的任务代码即可何必再用GCD同步执行该任务呢!文章中提这种问题就是一种对GCD用法的研究,小伙伴们不要太在意。

同步+并行队列:不会开启新的线程,虽然队列是并行队列但是同步执行不具备开启新线程的条件,所以只能在该线程中依次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
- (void)viewDidLoad {
[super viewDidLoad];
// 创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
NSLog(@"task1--%@",[NSThread currentThread]);
});
dispatch_sync(concurrentQueue, ^{
NSLog(@"task2--%@",[NSThread currentThread]);
});
dispatch_sync(concurrentQueue, ^{
NSLog(@"task3--%@",[NSThread currentThread]);
});
dispatch_sync(concurrentQueue, ^{
NSLog(@"task4--%@",[NSThread currentThread]);
});
// 嵌套同步并行执行
dispatch_sync(concurrentQueue, ^{
NSLog(@"task5 start");
NSLog(@"task5--%@",[NSThread currentThread]);
dispatch_sync(concurrentQueue, ^{
NSLog(@"task6--%@",[NSThread currentThread]);
});
NSLog(@"task5 end");
});
2017-04-17 23:01:40.527 MultiThreadDemo[1087:69003] task1--<NSThread: 0x60800007a140>{number = 1, name = main}
2017-04-17 23:01:40.527 MultiThreadDemo[1087:69003] task2--<NSThread: 0x60800007a140>{number = 1, name = main}
2017-04-17 23:01:40.527 MultiThreadDemo[1087:69003] task3--<NSThread: 0x60800007a140>{number = 1, name = main}
2017-04-17 23:01:40.527 MultiThreadDemo[1087:69003] task4--<NSThread: 0x60800007a140>{number = 1, name = main}
2017-04-17 23:01:40.527 MultiThreadDemo[1087:69003] task5 start
2017-04-17 23:01:40.528 MultiThreadDemo[1087:69003] task5--<NSThread: 0x60800007a140>{number = 1, name = main}
2017-04-17 23:01:40.528 MultiThreadDemo[1087:69003] task6--<NSThread: 0x60800007a140>{number = 1, name = main}
2017-04-17 23:01:40.528 MultiThreadDemo[1087:69003] task5 end

如上同步并行执行,由于没有开启新的线程,task1-6在主线程中顺序执行,由嵌套同步并行执行代码可看出放入到并行队列的任务会立即执行,直到执行完成才接着做原来任务继续执行(同步执行下)。

异步+串行队列:可以开启一条线程,任务在串行队列中依次被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
- (void)viewDidLoad {
[super viewDidLoad];
// 创建串行队列
__block dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"task1--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 5; index++) {
NSLog(@"task1--run%zd",index);
[NSThread sleepForTimeInterval:0.2];
}
});
dispatch_async(serialQueue, ^{
NSLog(@"task2--%@",[NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"task3--%@",[NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"task4--%@",[NSThread currentThread]);
});
// 嵌套异步串行执行,模拟相同串行队列下异步执行不创建线程情况
dispatch_async(serialQueue, ^{
NSLog(@"task5 start");
NSLog(@"task5--%@",[NSThread currentThread]);
// 打开下面代码会创建新的线程
// serialQueue = dispatch_queue_create("anotherSerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"task5.1--%@",[NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"task5.2--%@",[NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"task5.3--%@",[NSThread currentThread]);
});
for (NSInteger index = 0; index < 5; index++) {
NSLog(@"task5--run%zd",index);
}
NSLog(@"task5 end");
});
2017-04-17 23:53:38.777 MultiThreadDemo[1547:113364] task1--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:38.777 MultiThreadDemo[1547:113364] task1--run0
2017-04-17 23:53:38.978 MultiThreadDemo[1547:113364] task1--run1
2017-04-17 23:53:39.179 MultiThreadDemo[1547:113364] task1--run2
2017-04-17 23:53:39.383 MultiThreadDemo[1547:113364] task1--run3
2017-04-17 23:53:39.588 MultiThreadDemo[1547:113364] task1--run4
2017-04-17 23:53:39.790 MultiThreadDemo[1547:113364] task2--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:39.790 MultiThreadDemo[1547:113364] task3--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:39.791 MultiThreadDemo[1547:113364] task4--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:39.791 MultiThreadDemo[1547:113364] task5 start
2017-04-17 23:53:39.791 MultiThreadDemo[1547:113364] task5--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:39.791 MultiThreadDemo[1547:113364] task5--run0
2017-04-17 23:53:39.792 MultiThreadDemo[1547:113364] task5--run1
2017-04-17 23:53:39.792 MultiThreadDemo[1547:113364] task5--run2
2017-04-17 23:53:39.792 MultiThreadDemo[1547:113364] task5--run3
2017-04-17 23:53:39.792 MultiThreadDemo[1547:113364] task5--run4
2017-04-17 23:53:39.792 MultiThreadDemo[1547:113364] task5 end
2017-04-17 23:53:39.793 MultiThreadDemo[1547:113364] task5.1--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:39.793 MultiThreadDemo[1547:113364] task5.2--<NSThread: 0x608000265ac0>{number = 3, name = (null)}
2017-04-17 23:53:39.793 MultiThreadDemo[1547:113364] task5.3--<NSThread: 0x608000265ac0>{number = 3, name = (null)}

异步串行执行情况下,最多可创建一条新的线程(提供一个新的串行队列情况下),当串行队列相同时不会创建新的线程如嵌套异步串行执行所演示。在同一个串行队列的任务依次执行,也可以从task5和其子任务可看出,task5比5.1,5.2,5.3先加入队列中,需要等task5任务结束之后再去执行其后的子任务。如果将注释代码打开serialQueue = dispatch_queue_create("anotherSerialQueue", DISPATCH_QUEUE_SERIAL)就可演示非同一个串行队列的任务不必等到当前任务执行完成才能执行,当然这属于异步的范畴本就该如此。打开注释打印log如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2017-04-18 00:00:43.213 MultiThreadDemo[1595:119170] task1--<NSThread: 0x60800026df80>{number = 3, name = (null)}
2017-04-18 00:00:43.213 MultiThreadDemo[1595:119170] task1--run0
2017-04-18 00:00:43.418 MultiThreadDemo[1595:119170] task1--run1
2017-04-18 00:00:43.621 MultiThreadDemo[1595:119170] task1--run2
2017-04-18 00:00:43.823 MultiThreadDemo[1595:119170] task1--run3
2017-04-18 00:00:44.028 MultiThreadDemo[1595:119170] task1--run4
2017-04-18 00:00:44.231 MultiThreadDemo[1595:119170] task2--<NSThread: 0x60800026df80>{number = 3, name = (null)}
2017-04-18 00:00:44.231 MultiThreadDemo[1595:119170] task3--<NSThread: 0x60800026df80>{number = 3, name = (null)}
2017-04-18 00:00:44.232 MultiThreadDemo[1595:119170] task4--<NSThread: 0x60800026df80>{number = 3, name = (null)}
2017-04-18 00:00:44.232 MultiThreadDemo[1595:119170] task5 start
2017-04-18 00:00:44.232 MultiThreadDemo[1595:119170] task5--<NSThread: 0x60800026df80>{number = 3, name = (null)}
2017-04-18 00:00:44.233 MultiThreadDemo[1595:119170] task5--run0
2017-04-18 00:00:44.233 MultiThreadDemo[1595:119172] task5.1--<NSThread: 0x60800026ddc0>{number = 4, name = (null)}
2017-04-18 00:00:44.233 MultiThreadDemo[1595:119170] task5--run1
2017-04-18 00:00:44.233 MultiThreadDemo[1595:119172] task5.2--<NSThread: 0x60800026ddc0>{number = 4, name = (null)}
2017-04-18 00:00:44.234 MultiThreadDemo[1595:119170] task5--run2
2017-04-18 00:00:44.234 MultiThreadDemo[1595:119172] task5.3--<NSThread: 0x60800026ddc0>{number = 4, name = (null)}
2017-04-18 00:00:44.234 MultiThreadDemo[1595:119170] task5--run3
2017-04-18 00:00:44.234 MultiThreadDemo[1595:119170] task5--run4
2017-04-18 00:00:44.235 MultiThreadDemo[1595:119170] task5 end

异步+并行队列:会开启一条或多条线程(依赖加入到并行队列的任务数),但是不会无限制的开启新的线程,毕竟开启线程、管理线程也会浪费很多CPU资源。异步并行演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- (void)viewDidLoad {
[super viewDidLoad];
// 创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"task1--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task1--run:%zd",index);
}
});
dispatch_async(concurrentQueue, ^{
NSLog(@"task2--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"task3--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"task4--%@",[NSThread currentThread]);
});
// 异步并行嵌套调用,模拟相同并行队列异步执行会创建新的线程情况
dispatch_async(concurrentQueue, ^{
NSLog(@"task5 begin");
dispatch_async(concurrentQueue, ^{
NSLog(@"task5.1--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"task5.2--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"task5.3--%@",[NSThread currentThread]);
});
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task5--run:%zd",index);
}
NSLog(@"task5 end");
});
}
2017-04-18 11:00:33.395 MultiThreadDemo[2126:101886] task1--<NSThread: 0x60000007f2c0>{number = 3, name = (null)}
2017-04-18 11:00:33.395 MultiThreadDemo[2126:101884] task3--<NSThread: 0x60000007fcc0>{number = 5, name = (null)}
2017-04-18 11:00:33.395 MultiThreadDemo[2126:101900] task2--<NSThread: 0x60800007a000>{number = 4, name = (null)}
2017-04-18 11:00:33.395 MultiThreadDemo[2126:101886] task1--run:0
2017-04-18 11:00:33.395 MultiThreadDemo[2126:101884] task5 begin
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101886] task1--run:1
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101884] task5--<NSThread: 0x60000007fcc0>{number = 5, name = (null)}
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101883] task4--<NSThread: 0x60000006ac40>{number = 6, name = (null)}
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101884] task5--run:0
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101886] task5.1--<NSThread: 0x60000007f2c0>{number = 3, name = (null)}
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101900] task5.3--<NSThread: 0x60800007a000>{number = 4, name = (null)}
2017-04-18 11:00:33.396 MultiThreadDemo[2126:101919] task5.2--<NSThread: 0x60000007f980>{number = 7, name = (null)}
2017-04-18 11:00:33.397 MultiThreadDemo[2126:101884] task5--run:1
2017-04-18 11:00:33.397 MultiThreadDemo[2126:101884] task5 end

异步并行执行给我们的感觉就是你并不知道它什么时间会被调用,但总体上来看是各个线程交替执行符合并发的特点。这种执行方式可以大大的提高业务的执行效率提高CPU的性能,所以在开发中遇到多种耗时操作时使用异步并行处理。

以上是对异步、同步配合串行、并行队列的四种情况的详细介绍,演示囊括了开发中所遇到的所有基本情况。建议尽量理解这四种的用法!

如果你看到这里说明你已经能够运用GCD处理多线程了,除了那些基本的用法之外GCD还有很多其它的用法。以下的几个小模块都将围绕GCD扩展功能来叙述!

dispatch_after

GCD提供了延迟加入任务的方法,满足延迟执行的需求:

1
2
3
4
5
6
7
8
9
10
11
12
/** GCD延迟执行方法
* when 描述延迟时间
* queue 指定队列延迟添加任务
* block 任务代码块
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);
/** 创建dispatch_time_t类型
* when 参考时间DISPATCH_TIME_NOW:当前时间 DISPATCH_TIME_FOREVER:无穷大
* delta 偏移量
*/
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);

下面演示延迟执行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)dispatchAfter {
NSLog(@"task start");
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
NSLog(@"2 sec delay task!");
});
NSLog(@"task end");
}
2017-04-18 22:13:45.650 MultiThreadDemo[963:50222] task start
2017-04-18 22:13:45.650 MultiThreadDemo[963:50222] task end
2017-04-18 22:13:47.650 MultiThreadDemo[963:50222] 2 sec delay task!

这种显示创建dispatch_time_t的可以被类似下面的这个方法替代:

1
2
3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// delay methods
});

dispatch_once

dispatch_once提供了代码只被运行一次的功能,这种功能一般使用场景如:单例的创建,程序基础资源初始化,下面让我们来看看如何使用它吧!
GCD单例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static NSObject *_instance;
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}

dispatch_source

dispatch_source是一种用来处理操作系统底层级别事件的数据类型,它能够处理很多类型的事件这里我们只研究下Timer dispatch sources定时器类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** 创建事件
* type 事件类型
* handle 底层的系统处理监控
* mask
* queue 指定队列
*/
dispatch_source_t dispatch_source_create(dispatch_source_type_t type,uintptr_t handle,unsigned long mask,dispatch_queue_t queue);
/** 设置timer事件属性
* source 事件
* start 开始时间
* interval 定时器间隔
* leeway 允许误差
*/
void dispatch_source_set_timer(dispatch_source_t source,dispatch_time_t start,uint64_t interval,uint64_t leeway);
/** 设置事件触发任务
* source 事件
* handler 对应事件的任务
*/
void dispatch_source_set_event_handler(dispatch_source_t source,dispatch_block_t handler);
// 触发事件
void dispatch_resume(dispatch_object_t object);
// 取消事件
void dispatch_source_cancel(dispatch_source_t source);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定时器timer引用
@property (nonatomic,strong)dispatch_source_t timer;
// 开始timer
- (void)dispatchTimer {
// 创建事件
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 设置timer事件属性
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 事件处理
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"timer running!");
});
// 激活事件
dispatch_resume(_timer);
}
// 停止timer
- (void)stopDispatchTimer {
dispatch_source_cancel(_timer);
_timer = NULL;
}

其实在开发中我们最常用的定时器应该是NSTimer,NSTimer在runloop任务比较多的时候精度可能跟不上我们的需求,这时就可以用Timer dispatch sources来更精确的处理我们的定时任务了。dispatch_source还可以处理很多的事件,有兴趣的童鞋可以自己去网上查查相关资料。

dispatch_group

dispatch_group提供了任务组的概念,在组内的任务串行或并行全部完成之后再做统一处理。在日常开发中有时可能遇到这样的情况:在一个页面里面需要的数据一个接口给的不全,还需要调用另一个接口。两个接口返回的结果整合起来去刷新UI,因为这两个接口异步调用返回时间不确定普通处理方法比较麻烦。使用队列组处理这类问题就变得简单了。下面演示队列组的使用:

场景一:任务A先执行,A执行完成后任务B使用A所产生的数据做进一步数据处理,两个任务完成后回主线程再进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** 主线程调用groupSerail
* 串行队列中的任务依次执行
*/
- (void)groupSerail {
// 创建组
dispatch_group_t group = dispatch_group_create();
// 创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
__block NSString *resultString = @"";
// 任务A
dispatch_group_async(group, serialQueue, ^{
NSLog(@"taskA begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskA run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
resultString = [resultString stringByAppendingString:@"A"];
NSLog(@"taskA end!");
});
// 任务B
dispatch_group_async(group, serialQueue, ^{
NSLog(@"taskB begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskB run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
resultString = [resultString stringByAppendingString:@"B"];
NSLog(@"taskB end!");
});
// 任务组所有任务完成后任务回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"resultString:%@--%@",resultString,[NSThread currentThread]);
});
2017-04-19 18:30:47.553 MultiThreadDemo[1093:92225] taskA begin:--<NSThread: 0x600000272200>{number = 3, name = (null)}
2017-04-19 18:30:47.553 MultiThreadDemo[1093:92225] taskA run--0
2017-04-19 18:30:47.654 MultiThreadDemo[1093:92225] taskA run--1
2017-04-19 18:30:47.759 MultiThreadDemo[1093:92225] taskA end!
2017-04-19 18:30:47.759 MultiThreadDemo[1093:92225] taskB begin:--<NSThread: 0x600000272200>{number = 3, name = (null)}
2017-04-19 18:30:47.760 MultiThreadDemo[1093:92225] taskB run--0
2017-04-19 18:30:47.864 MultiThreadDemo[1093:92225] taskB run--1
2017-04-19 18:30:47.967 MultiThreadDemo[1093:92225] taskB end!
2017-04-19 18:30:47.968 MultiThreadDemo[1093:92188] resultString:AB--<NSThread: 0x60000007a900>{number = 1, name = main}

场景二:任务A和任务B各自执行(异步并发),执行后到主线程统一处理数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/** 主线程调用groupConcurrent
* 添加到并行队列的任务并发交替执行
*/
- (void)groupConcurrent {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
__block NSString *taskA = @"";
dispatch_group_async(group, concurrentQueue, ^{
NSLog(@"taskA begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskA run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
taskA = @"A";
NSLog(@"taskA end!");
});
__block NSString *taskB = @"";
dispatch_group_async(group, concurrentQueue, ^{
NSLog(@"taskB begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskB run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
taskB = @"B";
NSLog(@"taskB end!");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"resultString:%@%@--%@",taskA,taskB,[NSThread currentThread]);
});
}
2017-04-19 18:31:28.179 MultiThreadDemo[1128:93768] taskA begin:--<NSThread: 0x600000260100>{number = 3, name = (null)}
2017-04-19 18:31:28.179 MultiThreadDemo[1128:93785] taskB begin:--<NSThread: 0x608000264e40>{number = 4, name = (null)}
2017-04-19 18:31:28.180 MultiThreadDemo[1128:93768] taskA run--0
2017-04-19 18:31:28.180 MultiThreadDemo[1128:93785] taskB run--0
2017-04-19 18:31:28.283 MultiThreadDemo[1128:93768] taskA run--1
2017-04-19 18:31:28.283 MultiThreadDemo[1128:93785] taskB run--1
2017-04-19 18:31:28.387 MultiThreadDemo[1128:93768] taskA end!
2017-04-19 18:31:28.387 MultiThreadDemo[1128:93785] taskB end!
2017-04-19 18:31:28.387 MultiThreadDemo[1128:93734] resultString:AB--<NSThread: 0x60800006de00>{number = 1, name = main}

场景三:以上两个场景的混合使用,任务A和任务B异步并发执行,而任务A又分了两个子任务同步执行,最后将任务A和任务B的数据整合到主线程处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
- (void)groupConcurrentWithSerial {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__block NSString *taskA = @"";
__block NSString *taskB = @"";
dispatch_group_async(group, concurrentQueue, ^{
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_group_async(group, serialQueue, ^{
NSLog(@"taskA1 begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskA1 run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
taskA = @"A1";
NSLog(@"taskA1 end!");
});
dispatch_group_async(group, serialQueue, ^{
NSLog(@"taskA2 begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskA2 run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
taskA = [taskA stringByAppendingString:@"A2"];
NSLog(@"taskA2 end!");
});
});
dispatch_group_async(group, concurrentQueue, ^{
NSLog(@"taskB begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskB run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
taskB = @"B";
NSLog(@"taskB end!");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"resultString:%@%@--%@",taskA,taskB,[NSThread currentThread]);
});
}
2017-04-19 18:32:01.134 MultiThreadDemo[1154:94814] taskA1 begin:--<NSThread: 0x608000079c80>{number = 3, name = (null)}
2017-04-19 18:32:01.134 MultiThreadDemo[1154:94815] taskB begin:--<NSThread: 0x608000079c40>{number = 4, name = (null)}
2017-04-19 18:32:01.134 MultiThreadDemo[1154:94814] taskA1 run--0
2017-04-19 18:32:01.134 MultiThreadDemo[1154:94815] taskB run--0
2017-04-19 18:32:01.235 MultiThreadDemo[1154:94815] taskB run--1
2017-04-19 18:32:01.235 MultiThreadDemo[1154:94814] taskA1 run--1
2017-04-19 18:32:01.339 MultiThreadDemo[1154:94814] taskA1 end!
2017-04-19 18:32:01.339 MultiThreadDemo[1154:94815] taskB end!
2017-04-19 18:32:01.340 MultiThreadDemo[1154:94814] taskA2 begin:--<NSThread: 0x608000079c80>{number = 3, name = (null)}
2017-04-19 18:32:01.340 MultiThreadDemo[1154:94814] taskA2 run--0
2017-04-19 18:32:01.445 MultiThreadDemo[1154:94814] taskA2 run--1
2017-04-19 18:32:01.546 MultiThreadDemo[1154:94814] taskA2 end!
2017-04-19 18:32:01.547 MultiThreadDemo[1154:94782] resultString:A1A2B--<NSThread: 0x608000064c40>{number = 1, name = main}

dispatch_apply

dispatch_apply其实可以看作是同步执行dispatch_sync与任务组dispatch_group的组合体,适用于重复的任务体添加。既然是任务组又是同步执行,所以在dispatch_apply之后的业务都会等待它里面的任务都执行完了才会执行。下面对dispatch_apply的几种使用情况进行分析:

同步+dispatch_apply串行队列:不会开启新的线程就在原线程执行任务,第一个任务执行完后才会执行下一个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 主线程调用syncSerial方法
- (void)syncSerial {
dispatch_queue_t serialQueue = dispatch_queue_create("syncSerial", DISPATCH_QUEUE_SERIAL);
// 重复添加任务两次
dispatch_apply(2, serialQueue, ^(size_t index) {
NSLog(@"task%zd--%@",index+1,[NSThread currentThread]);
for (NSInteger time = 0; time < 3; time++) {
NSLog(@"task%zd--run%zd",index+1,time+1);
[NSThread sleepForTimeInterval:0.1];
}
});
NSLog(@"after dispatch_apply!");
}
2017-04-18 16:46:54.073 MultiThreadDemo[3631:285559] task1--<NSThread: 0x608000068d00>{number = 1, name = main}
2017-04-18 16:46:54.074 MultiThreadDemo[3631:285559] task1--run1
2017-04-18 16:46:54.175 MultiThreadDemo[3631:285559] task1--run2
2017-04-18 16:46:54.275 MultiThreadDemo[3631:285559] task1--run3
2017-04-18 16:46:54.377 MultiThreadDemo[3631:285559] task2--<NSThread: 0x608000068d00>{number = 1, name = main}
2017-04-18 16:46:54.377 MultiThreadDemo[3631:285559] task2--run1
2017-04-18 16:46:54.478 MultiThreadDemo[3631:285559] task2--run2
2017-04-18 16:46:54.579 MultiThreadDemo[3631:285559] task2--run3
2017-04-18 16:46:54.681 MultiThreadDemo[3631:285559] after dispatch_apply!

同步+dispatch_apply并行队列:可以开启新线程,根据任务和系统资源决定,任务可能在本线程中(main)执行,多个任务并发交替执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 主线程调用syncConcurrentApply方法
- (void)syncConcurrentApply {
dispatch_queue_t concurrentQueue = dispatch_queue_create("syncConcurrentApply", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(2, concurrentQueue, ^(size_t index) {
NSLog(@"task%zd--%@",index+1,[NSThread currentThread]);
for (NSInteger time = 0; time < 3; time++) {
NSLog(@"task%zd--run%zd",index+1,time+1);
[NSThread sleepForTimeInterval:0.1];
}
});
NSLog(@"after dispatch_apply!");
}
2017-04-18 16:53:12.054 MultiThreadDemo[3677:291669] task1--<NSThread: 0x600000072780>{number = 1, name = main}
2017-04-18 16:53:12.054 MultiThreadDemo[3677:291702] task2--<NSThread: 0x600000267dc0>{number = 3, name = (null)}
2017-04-18 16:53:12.054 MultiThreadDemo[3677:291669] task1--run1
2017-04-18 16:53:12.054 MultiThreadDemo[3677:291702] task2--run1
2017-04-18 16:53:12.155 MultiThreadDemo[3677:291669] task1--run2
2017-04-18 16:53:12.155 MultiThreadDemo[3677:291702] task2--run2
2017-04-18 16:53:12.256 MultiThreadDemo[3677:291669] task1--run3
2017-04-18 16:53:12.257 MultiThreadDemo[3677:291702] task2--run3
2017-04-18 16:53:12.361 MultiThreadDemo[3677:291669] after dispatch_apply!

异步+dispatch_apply串行队列:不会开启新线程,就在异步线程中任务依次执行,第一个任务执行完才会执行第二个任务。其实这种情况类似于同步+dispatch_apply串行队列,只不过换了个线程去执行任务罢了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 异步线程中调用asyncSerailApply方法
- (void)asyncSerailApply {
dispatch_queue_t serialQueue = dispatch_queue_create("asyncSerailApply", DISPATCH_QUEUE_SERIAL);
dispatch_apply(2, serialQueue, ^(size_t index) {
NSLog(@"task%zd--%@",index+1,[NSThread currentThread]);
for (NSInteger time = 0; time < 3; time++) {
NSLog(@"task%zd--run%zd",index+1,time+1);
[NSThread sleepForTimeInterval:0.1];
}
});
NSLog(@"after dispatch_apply!");
}
2017-04-18 16:55:11.895 MultiThreadDemo[3719:294228] task1--<NSThread: 0x600000262b00>{number = 3, name = (null)}
2017-04-18 16:55:11.896 MultiThreadDemo[3719:294228] task1--run1
2017-04-18 16:55:12.000 MultiThreadDemo[3719:294228] task1--run2
2017-04-18 16:55:12.105 MultiThreadDemo[3719:294228] task1--run3
2017-04-18 16:55:12.209 MultiThreadDemo[3719:294228] task2--<NSThread: 0x600000262b00>{number = 3, name = (null)}
2017-04-18 16:55:12.209 MultiThreadDemo[3719:294228] task2--run1
2017-04-18 16:55:12.314 MultiThreadDemo[3719:294228] task2--run2
2017-04-18 16:55:12.415 MultiThreadDemo[3719:294228] task2--run3
2017-04-18 16:55:12.519 MultiThreadDemo[3719:294228] after dispatch_apply!

异步+dispatch_apply并行队列:可以开启新线程,开线程数量跟任务和系统资源相关,除了开启新的线程,任务可在原异步线程中执行,多个任务并发交替执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 异步线程中调用asyncConcurrentApply
- (void)asyncConcurrentApply {
dispatch_queue_t concurrentQueue = dispatch_queue_create("asyncConcurrentApply", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(4, concurrentQueue, ^(size_t index) {
NSLog(@"task%zd--%@",index+1,[NSThread currentThread]);
for (NSInteger time = 0; time < 3; time++) {
NSLog(@"task%zd--run%zd",index+1,time+1);
[NSThread sleepForTimeInterval:0.1];
}
});
NSLog(@"after dispatch_apply!");
}
2017-04-18 16:56:02.388 MultiThreadDemo[3751:296095] task2--<NSThread: 0x60000027a1c0>{number = 4, name = (null)}
2017-04-18 16:56:02.388 MultiThreadDemo[3751:296140] task1--<NSThread: 0x608000273940>{number = 3, name = (null)}
2017-04-18 16:56:02.388 MultiThreadDemo[3751:296095] task2--run1
2017-04-18 16:56:02.388 MultiThreadDemo[3751:296140] task1--run1
2017-04-18 16:56:02.492 MultiThreadDemo[3751:296095] task2--run2
2017-04-18 16:56:02.492 MultiThreadDemo[3751:296140] task1--run2
2017-04-18 16:56:02.595 MultiThreadDemo[3751:296095] task2--run3
2017-04-18 16:56:02.595 MultiThreadDemo[3751:296140] task1--run3
2017-04-18 16:56:02.699 MultiThreadDemo[3751:296140] after dispatch_apply!

关于dispatch_apply相关介绍如上,需要我们注意的是在调起dispatch_apply的线程会被阻塞,需等待dispatch_apply内所有任务执行完成后再接着执行下面的代码。

dispatch_barrier

GCD的栅栏主要用来对多个任务进行分割,即先完成前面一部分然后在完成后面一部分,下面让我们看看相关使用情况。
场景:任务A,B,C,D都是进行异步执行,但是有个需求是先让A,B完成后再去执行C,D。听起来跟任务组类似,任务组是可以完成这类操作的,但是使用栅栏显得更容易点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (void)dispatchBarrier {
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"taskA--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"taskB--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"taskC--%@",[NSThread currentThread]);
});
// 栅栏
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"barrierTask--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"taskD--%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"taskE--%@",[NSThread currentThread]);
});
}
2017-04-19 20:41:50.206 MultiThreadDemo[1558:171823] taskB--<NSThread: 0x600000077e80>{number = 4, name = (null)}
2017-04-19 20:41:50.206 MultiThreadDemo[1558:171820] taskA--<NSThread: 0x600000077dc0>{number = 3, name = (null)}
2017-04-19 20:41:50.206 MultiThreadDemo[1558:171821] taskC--<NSThread: 0x608000079380>{number = 5, name = (null)}
2017-04-19 20:41:50.206 MultiThreadDemo[1558:171896] barrierTask--<NSThread: 0x608000079940>{number = 6, name = (null)}
2017-04-19 20:41:50.206 MultiThreadDemo[1558:171897] taskD--<NSThread: 0x608000079580>{number = 7, name = (null)}
2017-04-19 20:41:50.206 MultiThreadDemo[1558:171823] taskE--<NSThread: 0x600000077e80>{number = 4, name = (null)}

注意:The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_sync function.官方给的说明大概意思是栅栏效果需使用自定义的并发队列才有效果,使用串行队列或者全局并发队列只是一个同步执行任务一样。使用dispatch_barrier_async和dispatch_barrier_sync区别在于是否开启新的线程执行barrier里面添加的任务。

线程间通信

线程间的通讯在NSThread章节已经叙述过大概的使用场景,这里我们就不再赘述。按照之前的例子我们使用GCD来实现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)downloadImage {
NSURL *url = [NSURL URLWithString:@"https://raw.githubusercontent.com/CoderHann/imageSource/master/Blog/blog_usr_icon.jpeg"];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];
NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 网络访问成功
if (data && (error == nil)) {
UIImage *image = [UIImage imageWithData:data];
// 异步到主线程执行更新图片,不会阻塞当前线程
dispatch_async(dispatch_get_main_queue(), ^{
[self updateImage:image];
});
// 同步方式到主线程执行更新图片,阻塞当前线程,当完成updateImage之后再调用之后的方法
// dispatch_sync(dispatch_get_main_queue(), ^{
// [self updateImage:image];
// });
}
}];
线程安全

GCD的线程安全延续NSThread所使用的卖票演练例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)saleTickets {
NSLog(@"currentThread:%@",[NSThread currentThread]);
while (1) {
__block BOOL shouldExit = NO;
dispatch_sync(_serialQueue, ^{
NSThread *thread = [NSThread currentThread];
if (_leftTicketNum > 0) {
_leftTicketNum--;
NSLog(@"%@卖出一张票,剩余%zd张",thread.name,_leftTicketNum);
[NSThread sleepForTimeInterval:0.2];
} else {
shouldExit = YES;
NSLog(@"%@卖票结束",thread.name);
}
});
if (shouldExit) {
break;
}
}
}

NSOperation

NSOperation是基于GCD进行封装的具有面向对象特点的多线程管理方案,使用它不需要关心线程生命周期,任务处理变得更方便。如果GCD章节看过了那么使用NSOperation管理多线程也将变得清晰明了,除此之外而NSOperation为抽象类,在实际开发过程中可以使用它的两个子类或者自定义子类,这些operation映射到GCD中的任务,除了任务开启多线程还需要任务队列NSOperationQueue来进行多线程管理。在这一章节中我们将围绕NSOperation中的基础API任务&队列线程通讯其它特性进行详细讲解。

基础API

该小节是对相关的类以及其属性和方法进行介绍主要包括以下类:NSOperationNSBlockOperationNSInvocationOperationNSOperationQueue通过上面几个类的介绍给大家展示基础功能,对使用NSOperation多线程有初步的了解。

NSOperation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 任务抽象类
@interface NSOperation : NSObject
//
- (void)start;
//
- (void)main;
// 任务是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消任务
- (void)cancel;
// 是否在执行状态
@property (readonly, getter=isExecuting) BOOL executing;
// 是否完成
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isAsynchronous) BOOL asynchronous;
// 是否准备运行,一般运用在依赖相关任务
@property (readonly, getter=isReady) BOOL ready;
// 添加依赖的任务
- (void)addDependency:(NSOperation *)op;
// 移除依赖任务
- (void)removeDependency:(NSOperation *)op;
// 获取依赖任务组
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
/** 任务在队列中的优先级
* NSOperationQueuePriorityVeryLow
* NSOperationQueuePriorityLow
* NSOperationQueuePriorityNormal
* NSOperationQueuePriorityHigh
* NSOperationQueuePriorityVeryHigh
*/
@property NSOperationQueuePriority queuePriority;
@property (nullable, copy) void (^completionBlock)(void);
// 使用改方法会阻塞当前线程知道该任务执行完再执行下一个任务
- (void)waitUntilFinished;
@property double threadPriority;
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
// 任务名称
@property (nullable, copy) NSString *name;
@end

NSBlockOperation:

1
2
3
4
5
6
7
8
9
10
11
12
// 以block代码块生成operation任务,开发中使用较多
@interface NSBlockOperation : NSOperation
// 使用一个block块初始化一个任务operation
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
// 给operation添加任务block
- (void)addExecutionBlock:(void (^)(void))block;
// 该operation下的任务blocks
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;
@end

NSInvocationOperation:

1
2
3
4
5
@interface NSInvocationOperation : NSOperation
// 初始化任务方法
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
@end

NSOperationQueue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@interface NSOperationQueue : NSObject
// 添加任务op
- (void)addOperation:(NSOperation *)op;
// 加入指定的任务列表,wait参数描述是否等待所有任务执行完
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
// 添加任务块block
- (void)addOperationWithBlock:(void (^)(void))block;
// 该队列中添加的任务数组
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 该队列中的任务数
@property (readonly) NSUInteger operationCount;
// 队列内任务最大并发执行数(The maximum number of queued operations that can execute at the same time.)
@property NSInteger maxConcurrentOperationCount;
// 暂停执行任务
@property (getter=isSuspended) BOOL suspended;
// 队列名
@property (nullable, copy) NSString *name;
@property NSQualityOfService qualityOfService;
// 用来执行任务的调度队列
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue;
// 取消队列中所有的任务
- (void)cancelAllOperations;
// 阻塞当前线程,直到所有的排队中的执行操作执行完成。
- (void)waitUntilAllOperationsAreFinished;
// 类属性用于获取当前的任务队列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;
// 类属性用于获取主队列
@property (class, readonly, strong) NSOperationQueue *mainQueue;
@end

任务&队列

在了解相关基础后我们开始着手使用NSOperation开起多线程,由于NSOperation是基于GCD进行封装的,所以NSOperation与GCD有很多相似之处,比如任务和队列的概念。只不过这里的任务和队列其表现形式有一定的变化,下面针对任务和队列进行详细介绍。

任务

任务在NSOperation中的表现为任务对象,上面也提到过了通常我们使用的任务类是NSOperation的子类NSBlockOperationNSInvocationOperation以及自定义子类。那下面我们就按照这个顺序熟悉下任务的使用。

NSBlockOperation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)blockOperation {
// 创建任务
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// 任务代码块
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task1--%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.1];
}
}];
// 开始任务
[blockOperation start];
NSLog(@"task end!");
2017-04-24 13:55:14.958 MultiThreadDemo[2166:137330] task1--<NSThread: 0x60000006a6c0>{number = 1, name = main}
2017-04-24 13:55:15.059 MultiThreadDemo[2166:137330] task1--<NSThread: 0x60000006a6c0>{number = 1, name = main}
2017-04-24 13:55:15.161 MultiThreadDemo[2166:137330] task end!

上面代码演示一个最简单的任务执行,跟GCD的执行任务方式不一样需要我们手动调用start方法才开始执行任务,由打印日志看出我们的任务就在本线程中同步执行的。除此之外NSBlockOperation还提供了另一个方法用来追加任务addExecutionBlock:下面我们看看它的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)blockOperation {
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task1--%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.1];
}
}];
[blockOperation addExecutionBlock:^{
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task2--%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.1];
}
}];
[blockOperation start];
NSLog(@"task end!");
}
2017-04-24 14:33:45.449 MultiThreadDemo[2445:160983] task1--<NSThread: 0x60800007a780>{number = 1, name = main}
2017-04-24 14:33:45.449 MultiThreadDemo[2445:161044] task2--<NSThread: 0x608000272780>{number = 3, name = (null)}
2017-04-24 14:33:45.551 MultiThreadDemo[2445:160983] task1--<NSThread: 0x60800007a780>{number = 1, name = main}
2017-04-24 14:33:45.622 MultiThreadDemo[2445:161044] task2--<NSThread: 0x608000272780>{number = 3, name = (null)}
2017-04-24 14:33:45.790 MultiThreadDemo[2445:160983] task end!

使用addExecutionBlock:方法添加的任务块能够在其它线程进行并发执行,这个也是使用NSBlockOperation的一个特点。还有个需要我们注意的,虽然task2是异步执行但是根据打印的日志可以看到task end!在task2执行完后才执行,所以这种方式的异步执行会阻塞当前线程。

NSInvocationOperation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)invocationOperation {
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationTask) object:nil];
[invocationOperation start];
NSLog(@"task end!");
}
- (void)invocationTask {
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task1--%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.1];
}
}
2017-04-24 15:33:35.608 MultiThreadDemo[17607:254762] task1--<NSThread: 0x608000074780>{number = 1, name = main}
2017-04-24 15:33:35.709 MultiThreadDemo[17607:254762] task1--<NSThread: 0x608000074780>{number = 1, name = main}
2017-04-24 15:33:35.811 MultiThreadDemo[17607:254762] task end!

如上演示NSInvocationOperation的使用,同样是同步执行没有开启新的线程。

自定义子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 自定义任务继承自NSOperation
@interface CustomOperation()
// 目标执行对象
@property (nonatomic,weak)id target;
// 选择器
@property (nonatomic,assign)SEL selector;
@end
@implementation CustomOperation
// 工厂方法创建自定义任务
+ (instancetype)customOperationWithTarget:(id)target selector:(SEL)sel {
CustomOperation *operation = [[CustomOperation alloc] init];
operation.target = target;
operation.selector = sel;
return operation;
}
// 调试用
- (void)start {
NSLog(@"start begin!");
[super start];
NSLog(@"start end!");
}
// 执行任务主体
- (void)main {
// 添加自动释放出避免内存泄露
@autoreleasepool {
NSLog(@"main begin!");
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector];
}
NSLog(@"main end!");
}
}
2017-04-25 10:33:50.083 MultiThreadDemo[1283:72699] start begin!
2017-04-25 10:33:50.083 MultiThreadDemo[1283:72699] main begin!
2017-04-25 10:33:50.084 MultiThreadDemo[1283:72699] task1--<NSThread: 0x600000073980>{number = 1, name = main}
2017-04-25 10:33:50.184 MultiThreadDemo[1283:72699] task1--<NSThread: 0x600000073980>{number = 1, name = main}
2017-04-25 10:33:50.286 MultiThreadDemo[1283:72699] main end!
2017-04-25 10:33:50.286 MultiThreadDemo[1283:72699] start end!
2017-04-25 10:33:50.287 MultiThreadDemo[1283:72699] task end!

上面自定义任务模拟了类似NSInvocationOperation的调用方式,自定义任务主要是重写main方法,从打印的log我们可以看出是在start方法调用main方法的,所以要自定义异步并发方式的话可以重写start方法以及控制一些标示位,参考如下官方文档说明:In a concurrent operation, your start() method is responsible for starting the operation in an asynchronous manner. Whether you spawn a thread or call an asynchronous function, you do it from this method. Upon starting the operation, your start() method should also update the execution state of the operation as reported by the isExecuting property. You do this by sending out KVO notifications for the isExecuting key path, which lets interested clients know that the operation is now running. Your isExecuting property must also provide the status in a thread-safe manner. Upon completion or cancellation of its task, your concurrent operation object must generate KVO notifications for both the isExecuting and isFinished key paths to mark the final change of state for your operation. (In the case of cancellation, it is still important to update the isFinished key path, even if the operation did not completely finish its task. Queued operations must report that they are finished before they can be removed from a queue.) In addition to generating KVO notifications, your overrides of the isExecuting and isFinished properties should also continue to report accurate values based on the state of your operation.

队列

上面的小节我们演示了任务是如何单独使用的,我们要知道没有NSOperationQueue的配合是不会开启异步线程的(addExecutionBlock这个例外)。既然我们就是使用NSOperation来实现多线程的那我们这一小节就来了解下NSOperationQueue相关以及配合几种任务的使用吧。

队列简介:NSOperationQueue提供了两种队列一个是主队列自定义队列。在主队列中的任务都是同步执行的,而自定义队列中的任务根据队列属性设置可表现为同步或者异步执行。

1
2
// 主队列:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
1
2
// 自定义队列:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

添加任务:
有了队列我们就可以给队列添加任务了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (void)operationInCustomQueue {
NSOperationQueue *customQueue = [[NSOperationQueue alloc] init];
// maxConcurrentOperationCount默认为-1标示进行并发操作,设置为1表示队列内的任务依次完成后
customQueue.maxConcurrentOperationCount = 1;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task1 start--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task1 run--%zd",index);
[NSThread sleepForTimeInterval:0.2];
}
NSLog(@"task1 end--%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task2 start--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task2 run--%zd",index);
[NSThread sleepForTimeInterval:0.2];
}
NSLog(@"task2 end--%@",[NSThread currentThread]);
}];
[customQueue addOperation:op1];
[customQueue addOperation:op2];
}
2017-04-25 15:19:08.375 MultiThreadDemo[2836:215175] task1 start--<NSThread: 0x600000075d00>{number = 4, name = (null)}
2017-04-25 15:19:08.376 MultiThreadDemo[2836:215175] task1 run--0
2017-04-25 15:19:08.651 MultiThreadDemo[2836:215175] task1 run--1
2017-04-25 15:19:08.925 MultiThreadDemo[2836:215175] task1 end--<NSThread: 0x600000075d00>{number = 4, name = (null)}
2017-04-25 15:19:08.925 MultiThreadDemo[2836:215155] task2 start--<NSThread: 0x608000074b80>{number = 3, name = (null)}
2017-04-25 15:19:08.926 MultiThreadDemo[2836:215155] task2 run--0
2017-04-25 15:19:09.201 MultiThreadDemo[2836:215155] task2 run--1
2017-04-25 15:19:09.471 MultiThreadDemo[2836:215155] task2 end--<NSThread: 0x608000074b80>{number = 3, name = (null)}

如上log,我们看出了自定义队列情况下开启了新的线程,而新的线程的调度情况依赖maxConcurrentOperationCount的数值而定。使用默认的数值即不设定数值,开启新线程数量以及并发数都依赖系统可用资源。使用0时队列中的任务不会分配资源,使用1时队列中的任务依次执行,使用n(n>1)时队列的任务最高可并行执行数量为n,任务异步并发执行。

如下所示n=2的log:

1
2
3
4
5
6
7
8
9
10
11
12
2017-04-25 16:03:21.326 MultiThreadDemo[3472:254436] task2 start--<NSThread: 0x60000006ce00>{number = 4, name = (null)}
2017-04-25 16:03:21.326 MultiThreadDemo[3472:254437] task1 start--<NSThread: 0x600000068c80>{number = 3, name = (null)}
2017-04-25 16:03:21.326 MultiThreadDemo[3472:254436] task2 run--0
2017-04-25 16:03:21.326 MultiThreadDemo[3472:254437] task1 run--0
2017-04-25 16:03:21.600 MultiThreadDemo[3472:254436] task2 run--1
2017-04-25 16:03:21.600 MultiThreadDemo[3472:254437] task1 run--1
2017-04-25 16:03:21.871 MultiThreadDemo[3472:254436] task2 end--<NSThread: 0x60000006ce00>{number = 4, name = (null)}
2017-04-25 16:03:21.871 MultiThreadDemo[3472:254437] task1 end--<NSThread: 0x600000068c80>{number = 3, name = (null)}
2017-04-25 16:03:21.873 MultiThreadDemo[3472:254439] task3 start--<NSThread: 0x608000073b40>{number = 5, name = (null)}
2017-04-25 16:03:21.873 MultiThreadDemo[3472:254439] task3 run--0
2017-04-25 16:03:22.121 MultiThreadDemo[3472:254439] task3 run--1
2017-04-25 16:03:22.388 MultiThreadDemo[3472:254439] task3 end--<NSThread: 0x608000073b40>{number = 5, name = (null)}

下面看下在mainqueue中的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (void)operationInMainQueue {
NSOperationQueue *customQueue = [NSOperationQueue mainQueue];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task1 start--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task1 run--%zd",index);
[NSThread sleepForTimeInterval:0.2];
}
NSLog(@"task1 end--%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"task2 start--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"task2 run--%zd",index);
[NSThread sleepForTimeInterval:0.2];
}
NSLog(@"task2 end--%@",[NSThread currentThread]);
}];
[customQueue addOperation:op1];
[customQueue addOperation:op2];
}
2017-04-25 16:17:05.041 MultiThreadDemo[3592:267882] task1 start--<NSThread: 0x60800007bb80>{number = 1, name = main}
2017-04-25 16:17:05.041 MultiThreadDemo[3592:267882] task1 run--0
2017-04-25 16:17:05.241 MultiThreadDemo[3592:267882] task1 run--1
2017-04-25 16:17:05.443 MultiThreadDemo[3592:267882] task1 end--<NSThread: 0x60800007bb80>{number = 1, name = main}
2017-04-25 16:17:05.444 MultiThreadDemo[3592:267882] task2 start--<NSThread: 0x60800007bb80>{number = 1, name = main}
2017-04-25 16:17:05.444 MultiThreadDemo[3592:267882] task2 run--0
2017-04-25 16:17:05.646 MultiThreadDemo[3592:267882] task2 run--1
2017-04-25 16:17:05.847 MultiThreadDemo[3592:267882] task2 end--<NSThread: 0x60800007bb80>{number = 1, name = main}

没有开启新的线程就在主线程中队列中的任务依次执行!NSOperationQueue的添加任务除了addOperation:还有addOperationWithBlock:addOperations:waitUntilFinished:这些方法就不一一测试了有兴趣的童鞋可以自己用用。

其它特性

到此NSOperation的相关基础用法已经介绍完了,下面这些特性也是要熟练使用的,因为这些特性能够让NSOperation的操作变得更加的灵活,本节主要讲述任务依赖任务完成回调队列取消暂停恢复

任务依赖

任务依赖感觉一点任务组的意思,因为在之前的GCD中我们处理开发中多个接口的情况就是用了GCD的dispatch_group,而这个任务依赖也是处理这种情况的下面让我们看看是如何使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
- (void)taskDependency {
__block NSString *ATask = @"";
__block NSString *BTask = @"";
NSBlockOperation *taskA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA1 begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 2; index++) {
NSLog(@"taskA1 run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
ATask = @"A";
NSLog(@"taskA1 end!");
}];
NSBlockOperation *taskB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB begin:--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 5; index++) {
NSLog(@"taskB run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
BTask = @"B";
NSLog(@"taskB end!");
}];
NSBlockOperation *taskC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskC begin:--%@",[NSThread currentThread]);
// 给数据做拼装然后到页面上刷新数据
NSString *text = [NSString stringWithFormat:@"%@%@",ATask,BTask];
NSLog(@"result:%@",text);
// _aLabel.text = text;
NSLog(@"taskC end!");
}];
// 自定义队列
NSOperationQueue *customQueue = [[NSOperationQueue alloc] init];
// 主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
// 添加依赖
[taskC addDependency:taskA];
[taskC addDependency:taskB];
[customQueue addOperation:taskA];
[customQueue addOperation:taskB];
[mainQueue addOperation:taskC];
}
2017-04-25 17:47:00.642 MultiThreadDemo[4409:347772] taskA1 begin:--<NSThread: 0x60000007be00>{number = 4, name = (null)}
2017-04-25 17:47:00.642 MultiThreadDemo[4409:347791] taskB begin:--<NSThread: 0x608000269040>{number = 5, name = (null)}
2017-04-25 17:47:00.643 MultiThreadDemo[4409:347772] taskA1 run--0
2017-04-25 17:47:00.643 MultiThreadDemo[4409:347791] taskB run--0
2017-04-25 17:47:00.817 MultiThreadDemo[4409:347772] taskA1 run--1
2017-04-25 17:47:00.817 MultiThreadDemo[4409:347791] taskB run--1
2017-04-25 17:47:00.992 MultiThreadDemo[4409:347772] taskA1 end!
2017-04-25 17:47:00.992 MultiThreadDemo[4409:347791] taskB run--2
2017-04-25 17:47:01.141 MultiThreadDemo[4409:347791] taskB run--3
2017-04-25 17:47:01.319 MultiThreadDemo[4409:347791] taskB run--4
2017-04-25 17:47:01.493 MultiThreadDemo[4409:347791] taskB end!
2017-04-25 17:47:01.494 MultiThreadDemo[4409:347729] taskC begin:--<NSThread: 0x60000007aa80>{number = 1, name = main}
2017-04-25 17:47:01.494 MultiThreadDemo[4409:347729] result:AB
2017-04-25 17:47:01.494 MultiThreadDemo[4409:347729] taskC end!

任务依赖如上所示,这个演示了我们日常开发中很常见的一种场景就是一个页面的数据需要两个或者多个接口提供,通常处理是让两个接口同时去请求可以减少用户等待时间提高用户体验。等到两个接口的数据都回来的时候我们统一在主线程处理去进行UI刷新。根据log打印任务是按照我们的设计来执行的,任务依赖就到这吧。

任务完成回调/监听

在我们介绍基础APINSOperation任务中有一个属性@property (nullable, copy) void (^completionBlock)(void)完成回调,我们可以设置这个block对完成任务进行监听。通过这个完成时的监听可以统计一些信息或者对任务进行进一步的操作,下面让我们看看它的使用吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)taskCompletion {
NSBlockOperation *task = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA executed!");
}];
task.completionBlock = ^{
NSLog(@"taskA end!");
};
[task start];
}
2017-04-26 19:29:41.809 MultiThreadDemo[1261:87930] taskA executed!
2017-04-26 19:29:41.809 MultiThreadDemo[1261:87986] taskA end!

任务取消暂停恢复

取消:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 队列属性声明
@property (nonatomic,strong)NSOperationQueue *queue;
// 取消任务代码演示
- (void)taskCancle {
NSBlockOperation *taskA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA start!--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 6; index++) {
NSLog(@"taskA run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
NSLog(@"taskA end!");
}];
NSBlockOperation *taskB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB start!--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 6; index++) {
NSLog(@"taskB run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
NSLog(@"taskB end!");
}];
_queue = [[NSOperationQueue alloc] init];
_queue.maxConcurrentOperationCount = 1;
[_queue addOperation:taskA];
[_queue addOperation:taskB];
}
// 主动取消入口
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[_queue cancelAllOperations];
NSLog(@"主动取消队列任务");
}
// 任务正常进行
2017-04-26 19:45:50.255 MultiThreadDemo[1499:106209] taskA start!--<NSThread: 0x608000267b40>{number = 5, name = (null)}
2017-04-26 19:45:50.256 MultiThreadDemo[1499:106209] taskA run--0
2017-04-26 19:45:50.430 MultiThreadDemo[1499:106209] taskA run--1
2017-04-26 19:45:50.606 MultiThreadDemo[1499:106209] taskA run--2
2017-04-26 19:45:50.783 MultiThreadDemo[1499:106209] taskA run--3
2017-04-26 19:45:50.957 MultiThreadDemo[1499:106209] taskA run--4
2017-04-26 19:45:51.131 MultiThreadDemo[1499:106209] taskA run--5
2017-04-26 19:45:51.306 MultiThreadDemo[1499:106209] taskA end!
2017-04-26 19:45:51.306 MultiThreadDemo[1499:105968] taskB start!--<NSThread: 0x60000007ae00>{number = 6, name = (null)}
2017-04-26 19:45:51.307 MultiThreadDemo[1499:105968] taskB run--0
2017-04-26 19:45:51.482 MultiThreadDemo[1499:105968] taskB run--1
2017-04-26 19:45:51.654 MultiThreadDemo[1499:105968] taskB run--2
2017-04-26 19:45:51.828 MultiThreadDemo[1499:105968] taskB run--3
2017-04-26 19:45:52.002 MultiThreadDemo[1499:105968] taskB run--4
2017-04-26 19:45:52.176 MultiThreadDemo[1499:105968] taskB run--5
2017-04-26 19:45:52.313 MultiThreadDemo[1499:105968] taskB end!
}
// 中途取消任务情况
2017-04-26 19:54:15.769 MultiThreadDemo[1499:113796] taskA start!--<NSThread: 0x60800006d500>{number = 7, name = (null)}
2017-04-26 19:54:15.769 MultiThreadDemo[1499:113796] taskA run--0
2017-04-26 19:54:15.944 MultiThreadDemo[1499:113796] taskA run--1
2017-04-26 19:54:16.115 MultiThreadDemo[1499:113796] taskA run--2
2017-04-26 19:54:16.290 MultiThreadDemo[1499:113796] taskA run--3
2017-04-26 19:54:16.435 MultiThreadDemo[1499:105911] 主动取消队列任务
2017-04-26 19:54:16.464 MultiThreadDemo[1499:113796] taskA run--4
2017-04-26 19:54:16.638 MultiThreadDemo[1499:113796] taskA run--5
2017-04-26 19:54:16.810 MultiThreadDemo[1499:113796] taskA end!

注意:任务取消除了队列的cancelAllOperations方法,其本身也有一个取消的方法cancel。这里演示队列层面上的操作,根据日志我们可以得知分配到线程的任务取消方法是没有用的。

暂停&恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 任务暂停以及恢复演示
- (void)taskSuspendAndContinue {
NSBlockOperation *taskA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA start!--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 6; index++) {
NSLog(@"taskA run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
NSLog(@"taskA end!");
}];
NSBlockOperation *taskB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB start!--%@",[NSThread currentThread]);
for (NSInteger index = 0; index < 6; index++) {
NSLog(@"taskB run--%zd",index);
[NSThread sleepForTimeInterval:0.1];
}
NSLog(@"taskB end!");
}];
_queue = [[NSOperationQueue alloc] init];
_queue.maxConcurrentOperationCount = 1;
[_queue addOperation:taskA];
[_queue addOperation:taskB];
}
// 暂停和恢复入口
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
_queue.suspended = !_queue.isSuspended;
if (_queue.isSuspended) {
NSLog(@"主动暂停队列任务");
} else {
NSLog(@"主动恢复队列任务");
}
}
// 暂停和恢复演示log
2017-04-26 20:13:15.031 MultiThreadDemo[1664:132806] taskA start!--<NSThread: 0x60000007c900>{number = 5, name = (null)}
2017-04-26 20:13:15.032 MultiThreadDemo[1664:132806] taskA run--0
2017-04-26 20:13:15.205 MultiThreadDemo[1664:132806] taskA run--1
2017-04-26 20:13:15.379 MultiThreadDemo[1664:132806] taskA run--2
2017-04-26 20:13:15.553 MultiThreadDemo[1664:132806] taskA run--3
2017-04-26 20:13:15.664 MultiThreadDemo[1664:129467] 主动暂停队列任务
2017-04-26 20:13:15.728 MultiThreadDemo[1664:132806] taskA run--4
2017-04-26 20:13:15.902 MultiThreadDemo[1664:132806] taskA run--5
2017-04-26 20:13:16.076 MultiThreadDemo[1664:132806] taskA end!
2017-04-26 20:13:18.522 MultiThreadDemo[1664:129467] 主动恢复队列任务
2017-04-26 20:13:18.523 MultiThreadDemo[1664:129765] taskB start!--<NSThread: 0x60800027b5c0>{number = 6, name = (null)}
2017-04-26 20:13:18.523 MultiThreadDemo[1664:129765] taskB run--0
2017-04-26 20:13:18.693 MultiThreadDemo[1664:129765] taskB run--1
2017-04-26 20:13:18.863 MultiThreadDemo[1664:129765] taskB run--2
2017-04-26 20:13:19.037 MultiThreadDemo[1664:129765] taskB run--3
2017-04-26 20:13:19.211 MultiThreadDemo[1664:129765] taskB run--4
2017-04-26 20:13:19.385 MultiThreadDemo[1664:129765] taskB run--5
2017-04-26 20:13:19.557 MultiThreadDemo[1664:129765] taskB end!

其实队列的暂停和队列的取消是有一样的地方的,比如分配到线程的任务是无法暂停的,暂停也是针对那些在队列中没有执行的任务。而暂停相比于取消有一个特点就是暂停掉的任务是可以重新开启恢复的,利用这个特点可以在很多的开放场景中运用的到。

多线程演练

使用NSOperation进行多线程的讲解基本到此为止,我们最后将经典的卖票列子使用NSOperation进行演示。这里不适用同步锁或者GCD中的相关方法,利用NSOperation相关的多线程同步来解决数据同步问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
- (void)initalSaleTicket {
// 初始化票数以及任务队列
_leftTicketNum = 10;
_queue = [[NSOperationQueue alloc] init];
_queue.maxConcurrentOperationCount = 1;
// 北京窗口
NSBlockOperation *beijing = [NSBlockOperation blockOperationWithBlock:^{
[self saleTickets:@"北京"];
}];
// 上海窗口
NSBlockOperation *shanghai = [NSBlockOperation blockOperationWithBlock:^{
[self saleTickets:@"上海"];
}];
// 杭州窗口
NSBlockOperation *hangzhou = [NSBlockOperation blockOperationWithBlock:^{
[self saleTickets:@"杭州"];
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 开启异步售票模式
[queue addOperation:beijing];
[queue addOperation:shanghai];
[queue addOperation:hangzhou];
}
- (void)saleTickets:(NSString *)window {
NSBlockOperation *saleOperation = [NSBlockOperation blockOperationWithBlock:^{
if (_leftTicketNum > 0) {
_leftTicketNum--;
NSLog(@"%@卖出一张票,剩余%zd张--%@",window,_leftTicketNum,[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.2];
}
}];
saleOperation.completionBlock = ^{
[_queue addOperationWithBlock:^{
if (_leftTicketNum > 0) {
[self saleTickets:window];
} else {
NSLog(@"%@卖票结束!",window);
}
}];
};
[_queue addOperation:saleOperation];
}
2017-04-26 20:19:46.623 MultiThreadDemo[4878:193430] 北京卖出一张票,剩余9张--<NSThread: 0x60800026db40>{number = 3, name = (null)}
2017-04-26 20:19:46.897 MultiThreadDemo[4878:193430] 杭州卖出一张票,剩余8张--<NSThread: 0x60800026db40>{number = 3, name = (null)}
2017-04-26 20:19:47.166 MultiThreadDemo[4878:193412] 上海卖出一张票,剩余7张--<NSThread: 0x608000260900>{number = 4, name = (null)}
2017-04-26 20:19:47.428 MultiThreadDemo[4878:193430] 北京卖出一张票,剩余6张--<NSThread: 0x60800026db40>{number = 3, name = (null)}
2017-04-26 20:19:47.630 MultiThreadDemo[4878:193430] 杭州卖出一张票,剩余5张--<NSThread: 0x60800026db40>{number = 3, name = (null)}
2017-04-26 20:19:47.834 MultiThreadDemo[4878:193430] 上海卖出一张票,剩余4张--<NSThread: 0x60800026db40>{number = 3, name = (null)}
2017-04-26 20:19:48.038 MultiThreadDemo[4878:193412] 北京卖出一张票,剩余3张--<NSThread: 0x608000260900>{number = 4, name = (null)}
2017-04-26 20:19:48.240 MultiThreadDemo[4878:193409] 杭州卖出一张票,剩余2张--<NSThread: 0x600000271740>{number = 5, name = (null)}
2017-04-26 20:19:48.441 MultiThreadDemo[4878:193410] 上海卖出一张票,剩余1张--<NSThread: 0x6000002717c0>{number = 6, name = (null)}
2017-04-26 20:19:48.645 MultiThreadDemo[4878:193412] 北京卖出一张票,剩余0张--<NSThread: 0x608000260900>{number = 4, name = (null)}
2017-04-26 20:19:48.848 MultiThreadDemo[4878:193412] 北京卖票结束!
2017-04-26 20:19:48.850 MultiThreadDemo[4878:193412] 杭州卖票结束!
2017-04-26 20:19:48.851 MultiThreadDemo[4878:193412] 上海卖票结束!

小结

本文经过了数周的间断性更新,终于将iOS多线程pthreadNSThreadGCDNSOperation的使用方法、使用步骤以及注意点都给总结梳理了一番。如果你是一点一点跟着文章中的演示代码来的我觉得你应该已经能够熟练的使用iOS中这几种多线程了,更深入的研究还有待小伙伴不断的努力。下面我们在对这几种方法的优缺点简单的描述来结束该篇博客:

pthread:使用极少
-优:纯C语言实现效率高,跨平台
-缺:代码可读性差,需要开发管理线程生命周期

NSThread:使用比较少
-优:轻量级的管理多线程方式
-缺:需要开发手动管理线程的生命周期,线程同步系统开销较多

GCD:使用很多
-优:处理多线程效率高,能够适用多核CPU处理,不需要管理线程的生命周期让开发把更多精力放到业务上
-缺:C语言API代码可读性不强

NSOperation:使用很多
-优:底层封装GCD 具有面向对象特性,不需要管理线程,将执行的操作抽象为任务对象操作更加的方便,能够灵活控控制任务的进行方向。

以上是对iOS多线程的总结,由于是自己的理解上写的这篇文章,如果你发现本文出现了错误的地方还请及时联系我及时更正!