iOS多线程

iOS多线程

前言

在这篇文章中我整理了iOS多线程开发需要的一些知识点、方法和事项。本文主要讲述三部分 NSThread、Operation 和 GCD。

本文不算是原创,是一些我觉得写的比较有质量文章的综合,文中摘抄了这些文章中的部分内容,在文中和文章末尾我已标注出处,之所以要摘抄是因为在需要之时,打开URL 是404 😂😂😂。我综合了这些文中给自己学习之用,也给大家方便查看之用,如有错误之处,见谅,不过欢迎指出。

基本概念

我们先了解一线iOS多线程的一些基本概念(这些基本概念参考了这里):

  • 进程(process):指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程(说白了就是:用于指代一个可执行程序,他可以包含多个线程)。另外,当一个进程的主线程退出时,这个进程就结束了;
  • 线程(thread):指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads ;
  • 任务(task),指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。


串行 和 并发

从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。

  • 串行:指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;
  • 并发:指的是允许多个任务在一段时间内同时执行。


同步 和 异步

同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前线程。

  • 同步操作:等待操作执行完成后再继续执行接下来的代码;
  • 异步操作:它会在调用后立即返回,不会等待操作的执行结果。


队列 和 线程

在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理


iOS中的几种多线程类型

iOS 中其实目前有 4 种多线程,他们分别是:

  • Pthreads:非常底层的东东;
  • NSThread:封装性最差,最偏向于底层,主要基于thread使用;
  • GCD:基于C的API,直接使用比较方便,主要基于task使用;
  • NSOperation & NSOperationQueue:基于GCD封装的NSObject对象,对于复杂的多线程项目使用比较方便,主要基于队列使用。

下面开始介绍这几种类型

Pthreads

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

这个需要自己管理线程的生命周期,创建和销毁,写起来也是相当麻烦,若非是写一些非常底层的几乎是用不到的,这里暂时略过,如需要可自行去理解。

NSThread

NSThread是苹果封装的,并且是面向对象的,这对我们来说就简便了许多,但是它的生命周期还是需要我们手动管理的;NSThread除Pthreads之外唯一一个基于线程封装的,每一个NSThread对象代表着一个线程。(这部分参考了这里)

线程的创建

NSThread提供了2种创建线程的方法:

1
2
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
  • detach方法直接创建并启动一个线程去Selector,由于没有返回值,如果需要获取新创建的Thread,需要在执行的Selector中调用-[NSThread currentThread]获取;
  • init方法初始化线程并返回,线程的入口函数由Selector传入。线程创建出来之后需要手动调用-start方法启动;

    线程操作

    NSThread给线程提供的主要操作方法有启动,睡眠,取消,退出

    启动

    使用init方法将线程创建出来之后,线程并不会立即运行,需要手动调用-start方法才会启动线程:
1
- (void)start NS_AVAILABLE(10_5, 2_0);

在启动之前可以设置 线程的名称(当然启动后也可以在Selecter 中设置也可以):

1
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

不过优先级 需要在 start 之前 设置,否则会无效:

1
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);


睡眠

NSThread提供了2个让线程睡眠的方法:

  • 根据NSDate传入睡眠时间;
  • 直接传入NSTimeInterval
1
2
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

这里讲一下 sleepUntilDate: 和 runloop的runUntilDate: 上的一些区别:

  • sleepUntilDate:相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务;
  • runUntilDate:虽然会阻塞线程,阻塞过程中并不妨碍新任务的执行。当有新任务的时候,会先执行接收到的新任务,新任务执行完之后,如果时间到了,再继续执行runUntilDate:之后的代码;


取消

NSThread提供了一个取消的方法和一个属性:

1
2
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);

这里 cancel 方法要注意:调用-cancel方法并不会立刻取消线程,它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在main函数中自己实现;

要实现取消的功能,我们需要自己在线程的main函数中定期检查isCancelled状态来判断线程是否需要退出,当isCancelled为YES的时候,需要手动退出。

退出

-exit函数可以让线程立即退出。

停止方法会立即终止除主线程以外所有线程(无论是否在执行任务)并退出,需要在掌控所有线程状态的情况下调用此方法,否则可能会导致内存问题。

1
+ (void)exit;


主线程和当前线程

NSThread提供了非常方便的获取和判断主线程的API:

1
2
3
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);

获取当前线程:

1
+ (NSThread *)currentThread;

是否为多线程:

1
+ (BOOL)isMultiThreaded;

关于isMultiThreaded 苹果在这里作了解释(相信你能看懂 😜😜😜):

1
2
3
4
5
Return Value
YES if the application is multithreaded, NO otherwise.

Discussion
An application is considered multithreaded if a thread was ever detached from the main thread using either detachNewThreadSelector:toTarget:withObject: or start. If you detached a thread in your application using a non-Cocoa API, such as the POSIX or Multiprocessing Services APIs, this method could still return NO. The detached thread does not have to be currently running for the application to be considered multithreaded—this method only indicates whether a single thread has been spawned.


线程优先级

NSThread有4个优先级的API:

这两个用于设置和获取当前线程的优先级:

1
2
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

后两个通过对象设置和获取优先级:

1
2
@property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started

在iOS 8 之前:

1
[NSThread setThreadPriority:1.0];

这个方法的优先级的数值设置让人困惑,因为你不知道你应该设置多大的值是比较合适的,因此在iOS8之后,threadPriority添加了一句注释:To be deprecated; use qualityOfService below

意为iOS 8以后推荐使用qualityOfService属性:

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21, 最高优先级,用于用户交互事件
NSQualityOfServiceUserInitiated = 0x19, 次高优先级,用于用户需要马上执行的事件
NSQualityOfServiceUtility = 0x11, 普通优先级,用于普通任务
NSQualityOfServiceBackground = 0x09, 最低优先级,用于不重要的任务
NSQualityOfServiceDefault = -1 默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
} NS_ENUM_AVAILABLE(10_10, 8_0);

比如给线程设置次高优先级:

1
[newThread setQualityOfService:NSQualityOfServiceUserInitiated];


线程通讯

创建线程之后,经常需要从主线程把耗时的任务丢给辅助线程,当任务完成之后辅助线程再把结果传回主线程传,这些线程通讯一般用的都是perform方法:

1
2
3
4
5
6
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes

①:将selector丢给主线程执行,可以指定runloop mode

②:将selector丢给主线程执行,runloop mode默认为common mode

③:将selector丢个指定线程执行,可以指定runloop mode

④:将selector丢个指定线程执行,runloop mode默认为default mode

一般用③④方法将任务丢给辅助线程,任务执行完成之后再使用①②方法将结果传回主线程

注意:perform方法只对拥有runloop的线程有效,如果创建的线程没有添加runloop,perform的selector将无法执行。


线程通知

NSThread有三个线程相关的通知:

1
2
3
FOUNDATION_EXPORT NSString * const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSString * const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSString * const NSThreadWillExitNotification;
  • NSWillBecomeMultiThreadedNotification:由当前线程派生出第一个其他线程时发送;
  • NSDidBecomeSingleThreadedNotification:暂时不知道;
  • NSThreadWillExitNotification:线程退出时发送;


NSThread的简单使用

NSThread 创建还是很简单的:

1
2
3
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
thread.name = @"com.myCreate.www";
[thread start];

在线程启动之后会首先执行-threadAction,正常情况下threadAction方法执行结束之后,线程就会退出。为了线程可以长期复用接收消息,我们需要给线程加上runLoop

1
2
3
4
5
6
7
8
9
- (void)threadAction
{
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (![[NSThread currentThread] isCancelled]) {
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
}
  • 自定义的线程默认是没有runloop的,调用-currentRunLoop,方法内部会为线程创建runloop;
  • 如果没有数据源,runloop会在启动之后会立刻退出。所以需要给runloop添加一个数据源,这里添加的是NSPort数据源;
  • 定期检查isCancelled,当外部调用-cancel方法将isCancelled置为YES的时候,线程可以退出;


结束线程

当我们想要结束线程的时候,我们可以使用CFRunLoopStop()配合-cancel来结束线程:

1
2
[[NSThread currentThread] cancel];
CFRunLoopStop(CFRunLoopGetCurrent());


NSThread的一个小例子

这个例子很好的说明了NSThread使用时要注意些什么。

这个例子是:模拟售票这个例子来自这

情景:某演唱会门票发售,在广州和北京均开设窗口进行销售

下面是主要代码:新建两个线程

1
2
3
4
5
6
7
8
NSLog(@"***************售票开始****************");
NSThread *saleTicketsWindow_1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketsWindow_Action) object:nil];
saleTicketsWindow_1.name = @"北京售票中心";
[saleTicketsWindow_1 start];

NSThread *saleTicketsWindow_2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketsWindow_Action) object:nil];
saleTicketsWindow_2.name = @"广州售票中心";
[saleTicketsWindow_2 start];

开始执行售票

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)saleTicketsWindow_Action
{
while (1) {
if (_ticketCount > 0) {
_ticketCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"***************售票完成****************");
break;
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2016-09-10 17:54:22.636 NSThreadTest[2323:1382484] 剩余票数:9 窗口:广州售票中心
2016-09-10 17:54:22.636 NSThreadTest[2323:1382483] 剩余票数:10 窗口:北京售票中心
2016-09-10 17:54:22.841 NSThreadTest[2323:1382483] 剩余票数:7 窗口:北京售票中心
2016-09-10 17:54:22.841 NSThreadTest[2323:1382484] 剩余票数:8 窗口:广州售票中心
2016-09-10 17:54:23.043 NSThreadTest[2323:1382484] 剩余票数:5 窗口:广州售票中心
2016-09-10 17:54:23.043 NSThreadTest[2323:1382483] 剩余票数:6 窗口:北京售票中心
2016-09-10 17:54:23.244 NSThreadTest[2323:1382484] 剩余票数:4 窗口:广州售票中心
2016-09-10 17:54:23.244 NSThreadTest[2323:1382483] 剩余票数:3 窗口:北京售票中心
2016-09-10 17:54:23.449 NSThreadTest[2323:1382483] 剩余票数:1 窗口:北京售票中心
2016-09-10 17:54:23.449 NSThreadTest[2323:1382484] 剩余票数:2 窗口:广州售票中心
2016-09-10 17:54:23.655 NSThreadTest[2323:1382483] 剩余票数:0 窗口:北京售票中心
2016-09-10 17:54:23.655 NSThreadTest[2323:1382484] ***************售票完成****************
2016-09-10 17:54:23.656 NSThreadTest[2323:1382484] 线程退出了:广州售票中心
2016-09-10 17:54:23.861 NSThreadTest[2323:1382483] ***************售票完成****************
2016-09-10 17:54:23.861 NSThreadTest[2323:1382483] 线程退出了:北京售票中心

可以看到,票的销售过程中出现了剩余数量错乱的情况,这就是线程同步问题。

售票是一个典型的需要线程同步的场景,由于售票渠道有很多,而票的资源是有限的,当多个渠道在短时间内卖出大量的票的时候,如果没有同步机制来管理票的数量,将会导致票的总数和售出票数对应不上的错误。

iOS实现线程加锁有几种方式,现在使用NSLock或@synchronized两种方式都可行

使用NSLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
while (1) {
[_lock lock];
if (_ticketCount > 0) {
_ticketCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"***************售票完成****************");
[_lock unlock];
break;
}
[_lock unlock];
}

使用@synchronized:

1
2
3
4
5
6
7
8
9
10
11
12
while (1) {
@synchronized(self) {
if (_ticketCount > 0) {
_ticketCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"***************售票完成****************");
break;
}
}
}

NSOperation

NSOperation 只是一个抽象类,所以不能封装任务,因此,如果我们想要使用它来执行具体任务的话,就必须创建自己的子类或使用它的 2 个子类。分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。

NSInvocationOperation

下面是NSInvocationOperation的简单使用,可以看到 NSInvocationOperation 开始任务之后是在主线程执行任务的。

1
2
3
4
5
6
7
8
9
//1.创建对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction) object:nil];
//2.开始执行
[operation start];
//3.结果
/*
2016-05-06 05:53:43.219 NSOperationTest[5885:2491863] NSInvocationOperation --- <NSThread: 0x7fa269c06dc0>{number = 1, name = main}
是在主线程
*/

NSBlockOperation

NSBlockOperation 默认会在当前线程执行任务。但是 NSBlockOperation 还有一个方法:addExecutionBlock: ,通过这个方法可以给 Operation 添加多个执行 Block。这样 Operation 中的任务 会并发执行,它会 在主线程和其它的多个线程执行这些任务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation --- %@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[blockOperation addExecutionBlock:^{
NSLog(@"NSBlockOperation 第%d次:%@", i, [NSThread currentThread]);
}];
}
[blockOperation start];

2016-05-06 05:57:46.227 NSBlockOperationTest[5921:2510961] NSBlockOperation --- <NSThread: 0x7fc4c0401af0>{number = 1, name = main}

2016-05-06 06:01:25.939 NSBlockOperationTest[5939:2525676] NSBlockOperation 第2次:<NSThread: 0x7fb6cbe04590>{number = 1, name = main}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525676] NSBlockOperation 第3次:<NSThread: 0x7fb6cbe04590>{number = 1, name = main}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525676] NSBlockOperation 第4次:<NSThread: 0x7fb6cbe04590>{number = 1, name = main}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525717] NSBlockOperation 第0次:<NSThread: 0x7fb6cbf03040>{number = 2, name = (null)}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525710] NSBlockOperation 第1次:<NSThread: 0x7fb6cbc3f6e0>{number = 3, name = (null)}

注意:addExecutionBlock 方法必须在 start() 方法之前执行,否则就会报错:

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished'

NSOperationQueue

在 NSOperationQueue 中,任务不会在当前线程执行。当任务添加到队列,会自动调用任务的 start() 方法,所有任务是并行执行的。maxConcurrentOperationCount 可以设置最大任务并行数量,当设置为 1 时,在某种意义上就是串行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation --- %@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[blockOperation addExecutionBlock:^{
NSLog(@"NSBlockOperation 第%d次:%@", i, [NSThread currentThread]);
}];
}
[queue addOperation:blockOperation];
或者是:
[queue addOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"addOperationWithBlock 第%d次:%@", i, [NSThread currentThread]);
}
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"addOperationWithBlock %@", [NSThread currentThread]);
}];
}];

eg:NSOperation 有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 --- %@", [NSThread currentThread]);
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 --- %@", [NSThread currentThread]);
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 --- %@", [NSThread currentThread]);
}];
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperations:@[operation1,operation2,operation3] waitUntilFinished:NO];

结果:

1
2
3
2016-05-06 06:26:12.732 NSOperationQueueTest[6061:2650250] 下载图片 --- <NSThread: 0x7fc589e1d740>{number = 3, name = (null)}
2016-05-06 06:26:12.733 NSOperationQueueTest[6061:2650257] 打水印 --- <NSThread: 0x7fc589f01320>{number = 4, name = (null)}
2016-05-06 06:26:12.734 NSOperationQueueTest[6061:2650257] 上传图片 --- <NSThread: 0x7fc589f01320>{number = 4, name = (null)}

注意:不能添加相互依赖,会死锁,比如 A依赖B,B依赖A。

可以使用 removeDependency 来解除依赖关系。

可以在不同的队列之间依赖,反正就是这个依赖是添加到任务身上的,和队列没关系。

另外还有以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSOperation
BOOL executing; //判断任务是否正在执行
BOOL finished; //判断任务是否完成
void (^completionBlock)(void); //用来设置完成后需要执行的操作
- (void)cancel; //取消任务
- (void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕

NSOperationQueue

NSUInteger operationCount; //获取队列的任务数
- (void)cancelAllOperations; //取消队列中所有的任务
- (void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕
[queue setSuspended:YES]; // 暂停queue
[queue setSuspended:NO]; // 继续queue

关于NSOperationQueue中 :

1
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);

在这里有介绍其用法

这里也有解释

这里是苹果的解释:

1
2
3
4
Discussion
The default value of this property is nil. You can set the value of this property to an existing dispatch queue to have enqueued operations interspersed with blocks submitted to that dispatch queue.

The value of this property should only be set if there are no operations in the queue; setting the value of this property when operationCount is not equal to 0 raises an invalidArgumentException. The value of this property must not be the value returned by dispatch_get_main_queue(). The quality-of-service level set for the underlying dispatch queue overrides any value set for the operation queue's qualityOfService property.

自定义Operation

后面会有专门有一节介绍


GCD

Grand Central Dispatch 是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。

在 GCD 中,加入了两个非常重要的概念: 任务 和 队列。 任务即你要执行的,同步、异步和队列 前面已经讲解过。

放到串行队列的任务,GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。<br/>

放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。<br/>

创建队列

先讲概念:

GCD中的FIFO队列称为dispatch queue,用来保证先进来的任务先得到执行。

dispatch queue分三种

  • Serial:又叫private dispatch queues,同时只执行一个任务。Serial queue常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然各自是同步,但serial queue之间是并发执行。(DISPATCH_QUEUE_SERIAL)
  • Concurrent:又叫global dispatch queue,可以并发的执行多个任务,但执行完成顺序是随机的。(DISPATCH_QUEUE_CONCURRENT)
  • Main dispatch queue:全局可用的serial queue,在应用程序主线程上执行任务。

公开的5个不同队列:
运行在主线程中的main queue,
3个不同优先级的后台队列(High Priority Queue,Default Priority Queue,Low Priority Queue),
以及一个优先级更低的后台队列Background Priority Queue(用于I/O)

5种队列,主队列(main queue),四种通用调度队列,自己定制的队列。四种通用调度队列为

  • QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。
  • QOS_CLASS_USER_INITIATED: user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
  • QOS_CLASS_UTILITY: utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。
  • QOS_CLASS_BACKGROUND: background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。
主队列

这是一个特殊的 串行队列,它用于刷新 UI,任何需要刷新 UI 的工作都要在主队列执行.

1
dispatch_queue_t queue = dispatch_get_main_queue();
自定义的队列

其中第一个参数是标识符,用于 DEBUG 的时候标识唯一的队列,可以为空.

第二个参数用来表示创建的队列是串行的还是并行的,传入 DISPATCH_QUEUE_SERIAL 或 NULL 表示创建串行队列。传入 DISPATCH_QUEUE_CONCURRENT 表示创建并行队列。

1
2
3
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
dispatch_queue_attr_t _Nullable attr);
1
2
3
4
5
// 串行队列
dispatch_queue_t createQueue = dispatch_queue_create("com.testQueue", NULL);
dispatch_queue_t createQueue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
// 并行队列
dispatch_queue_t createQueue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);

系统还提供了 全局并行队列

1
dispatch_queue_t createQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

全局队列有这几种优先级:

1
2
3
4
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
创建任务

同步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_sync(queue, ^{

});
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"async之前 - %@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"async1 - %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"async2 - %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"async3 - %@",[NSThread currentThread]);
});
NSLog(@"async之后 - %@",[NSThread currentThread]);

log:

1
2
3
4
5
2016-05-05 07:03:01.137 syncTest[5181:1954382] async之前 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async1 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async2 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async3 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async之后 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}

注意:

1.同步队列无论设置是DISPATCH_QUEUE_SERIAL还是DISPATCH_QUEUE_CONCURRENT,任务都是在主线程执行,会阻塞主线程

2.dispatch_sync 中 queue 不能是 dispatch_get_main_queue() 即为主线程队列,否则会creash

1
2
3
4
5
NSLog(@"dispatch_sync  enter");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_sync action");
});
NSLog(@"dispatch_sync leave");

异步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_async(createQueue, ^{

});
//并行队列 DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"async之前 - %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"async1 - %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"async2 - %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"async3 - %@",[NSThread currentThread]);
});
NSLog(@"async之后 - %@",[NSThread currentThread]);

log:

1
2
3
4
5
2016-09-21 07:19:53.817 asyncTest[13930:8566716] async之前 - <NSThread: 0x600000066a40>{number = 1, name = main}
2016-09-21 07:19:53.818 asyncTest[13930:8566716] async之后 - <NSThread: 0x600000066a40>{number = 1, name = main}
2016-09-21 07:20:59.683 asyncTest[13930:8566864] async2 - <NSThread: 0x60800007d700>{number = 4, name = (null)}
2016-09-21 07:20:59.683 asyncTest[13930:8566862] async3 - <NSThread: 0x60000007a9c0>{number = 3, name = (null)}
2016-09-21 07:20:59.683 asyncTest[13930:8566867] async1 - <NSThread: 0x60000007ad00>{number = 5, name = (null)}

队列组

创建队列

group 可以分为 串行和并行的,区别在于DISPATCH_QUEUE_SERIAL和DISPATCH_QUEUE_CONCURRENT设置不同

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});

log: 串行,但是异步的,不会阻塞当前线程

1
2
3
4
5
6
7
8
9
10
2016-09-21 08:05:14.110 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.111 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.112 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.112 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.112 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.589 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.590 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.590 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.591 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.591 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}

并行:

1
2
3
4
5
6
7
8
9
10
2016-09-21 08:09:46.984 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.984 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.984 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.984 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.986 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
监听任务组事件的执行完毕

1.dispatch_group_notify

用来监听任务组事件的执行完毕, 异步执行闭包,不会阻塞

1
2
3
4
void
dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block)
;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});

dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});

NSLog(@"notify leave");
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"notify 完成 - %@", [NSThread currentThread]);
});

NSLog(@"notify leave");

log:

1
2
3
4
5
6
7
8
9
2016-09-22 07:03:18.150 GCDTest[19000:9806166] notify leave
2016-09-22 07:03:18.150 GCDTest[19000:9813919] group-01 - <NSThread: 0x60000026b700>{number = 5, name = group-01-thread}
2016-09-22 07:03:18.150 GCDTest[19000:9814304] group-03 - <NSThread: 0x608000277800>{number = 6, name = group-03-thread}
2016-09-22 07:03:18.151 GCDTest[19000:9806166] notify leave
2016-09-22 07:03:18.151 GCDTest[19000:9813919] group-01 - <NSThread: 0x60000026b700>{number = 5, name = group-01-thread}
2016-09-22 07:03:18.151 GCDTest[19000:9814304] group-03 - <NSThread: 0x608000277800>{number = 6, name = group-03-thread}
2016-09-22 07:03:18.152 GCDTest[19000:9813919] group-01 - <NSThread: 0x60000026b700>{number = 5, name = group-01-thread}
2016-09-22 07:03:18.152 GCDTest[19000:9814304] group-03 - <NSThread: 0x608000277800>{number = 6, name = group-03-thread}
2016-09-22 07:03:18.157 GCDTest[19000:9806166] notify 完成 - <NSThread: 0x60800007b6c0>{number = 1, name = main}

2.dispatch_group_wait

会阻塞当前进程,等所有任务都完成或等待超时。设置等待时间,在等待时间结束后,如果还没有执行完任务组,则返回。返回0代表执行成功,非0则执行失败

1
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

超时时间 可以根据需要设置,系统提供了两个宏:

1
2
#define DISPATCH_TIME_NOW (0ull)
#define DISPATCH_TIME_FOREVER (~0ull)

在此之前介绍一下时间:

iOS系统 GCD 中提供了一下几种时间

1
2
3
4
#define NSEC_PER_SEC  1000000000ull
#define NSEC_PER_MSEC 1000000ull
#define USEC_PER_SEC 1000000ull
#define NSEC_PER_USEC 1000ull

关键词解释:

  • NSEC:纳秒
  • USEC:微秒
  • MSEC:毫秒
  • SEC:秒
  • PER:每

故:

描述
NSEC_PER_SEC 每秒有多少纳秒
NSEC_PER_MSEC 每毫秒有多少纳秒
USEC_PER_SEC 每秒有多少微秒
NSEC_PER_USEC 每微秒有多少纳秒

所以,延时1秒可以写成如下几种:

1
2
3
4
dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC)
;

dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC);
dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC * NSEC_PER_USEC)
;

绝对时间,dispatch_group_wait 想等到2016年09月22日23:20:53 的时候超时,那么要使用 dispatch_walltime函数将NSdate 转化为dispatch_time,下面是方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (dispatch_time_t)switchDateToDispatch_time_tWithDate:(NSDate *)date
{
NSTimeInterval interval;
double second,subsecond;
struct timespec time;
dispatch_time_t milestone;
interval=[date timeIntervalSince1970];

subsecond = modf(interval, &second);
time.tv_sec = second;
time.tv_nsec= subsecond*NSEC_PER_SEC;

milestone = dispatch_walltime(&time, 0);
return milestone;
}

好了时间说完了,该说示例了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
NSLog(@"dispatch_group_wait 之前");
long value = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"dispatch_group_wait 之后 value = %ld",value);

log:

① DISPATCH_TIME_FOREVER 可以看到会阻塞当前线程直到任务全部完成

1
2
3
4
5
6
7
8
2016-09-22 07:15:14.211 GCDTest[19022:9870397] dispatch_group_wait 之前
2016-09-22 07:15:14.211 GCDTest[19022:9874488] group-01 - <NSThread: 0x608000268f40>{number = 5, name = group-01-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874995] group-03 - <NSThread: 0x608000268500>{number = 6, name = group-03-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874488] group-01 - <NSThread: 0x608000268f40>{number = 5, name = group-01-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874995] group-03 - <NSThread: 0x608000268500>{number = 6, name = group-03-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874488] group-01 - <NSThread: 0x608000268f40>{number = 5, name = group-01-thread}
2016-09-22 07:15:14.212 GCDTest[19022:9874995] group-03 - <NSThread: 0x608000268500>{number = 6, name = group-03-thread}
2016-09-22 07:15:14.212 GCDTest[19022:9870397] dispatch_group_wait 之后 value = 0

② DISPATCH_TIME_NOW dispatch_group_wait 并未阻塞当前线程 但是 返回非零值,表明任务执行失败或者超时,这明显是超时

1
2
3
4
5
6
7
8
2016-09-22 07:18:21.506 GCDTest[19041:9887622] dispatch_group_wait 之前
2016-09-22 07:18:21.506 GCDTest[19041:9891849] group-01 - <NSThread: 0x608000274700>{number = 3, name = group-01-thread}
2016-09-22 07:18:21.506 GCDTest[19041:9891894] group-03 - <NSThread: 0x608000274680>{number = 4, name = group-03-thread}
2016-09-22 07:18:21.506 GCDTest[19041:9887622] dispatch_group_wait 之后 value = 49
2016-09-22 07:18:21.506 GCDTest[19041:9891849] group-01 - <NSThread: 0x608000274700>{number = 3, name = group-01-thread}
2016-09-22 07:18:21.507 GCDTest[19041:9891894] group-03 - <NSThread: 0x608000274680>{number = 4, name = group-03-thread}
2016-09-22 07:18:21.507 GCDTest[19041:9891849] group-01 - <NSThread: 0x608000274700>{number = 3, name = group-01-thread}
2016-09-22 07:18:21.508 GCDTest[19041:9891894] group-03 - <NSThread: 0x608000274680>{number = 4, name = group-03-thread}

③ 自定义时间 会阻塞当前的线程

1
2
3
4
5
6
7
8
9
//这里让线程 睡眠2s
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
[NSThread sleepForTimeInterval:2];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
long value = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5*NSEC_PER_SEC)));
1
2
3
4
5
6
7
8
2016-09-22 07:37:32.478 GCDTest[19217:10005760] dispatch_group_wait 之前
2016-09-22 07:37:32.478 GCDTest[19217:10006060] group-01 - <NSThread: 0x608000274140>{number = 3, name = group-01-thread}
2016-09-22 07:37:32.479 GCDTest[19217:10006060] group-01 - <NSThread: 0x608000274140>{number = 3, name = group-01-thread}
2016-09-22 07:37:32.479 GCDTest[19217:10006060] group-01 - <NSThread: 0x608000274140>{number = 3, name = group-01-thread}
2016-09-22 07:37:34.482 GCDTest[19217:10006057] group-03 - <NSThread: 0x608000273f80>{number = 4, name = group-03-thread}
2016-09-22 07:37:34.483 GCDTest[19217:10006057] group-03 - <NSThread: 0x608000273f80>{number = 4, name = group-03-thread}
2016-09-22 07:37:34.483 GCDTest[19217:10006057] group-03 - <NSThread: 0x608000273f80>{number = 4, name = group-03-thread}
2016-09-22 07:37:34.484 GCDTest[19217:10005760] dispatch_group_wait 之后 value = 0

3.dispatch_group_enter dispatch_group_leave
这里 dispatch_group_enter 和 dispatch_group_leave 需要成对出现,否则dispatch_group_notify不会回调用(这里可能会崩溃,不过在我测试中没有发现)

①这是 dispatch_queue_t 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
[NSThread sleepForTimeInterval:2];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"notify 完成 - %@", [NSThread currentThread]);
});

②这是NSOperationQueue的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_group_t group = dispatch_group_create();
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
dispatch_group_enter(group);
NSBlockOperation *blockOperation_1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation 1 --- %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
NSBlockOperation *blockOperation_2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation 2 --- %@", [NSThread currentThread]);
dispatch_group_leave(group);
}];
[blockOperation_1 setName:@"blockOperation_1"];
[blockOperation_1 setName:@"blockOperation_2"];
[queue addOperation:blockOperation_1];
[queue addOperation:blockOperation_2];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"notify 完成 - %@", [NSThread currentThread]);
});
dispatch_once

dispatch_once 大家都不会陌生,但我认为还是有必要强调一次,dispatch_once_t 必须为静态变量

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// to do something
});
dispatch_after

dispatch_after 能让我们添加进队列的任务延时(这里说延时不严谨,后面会说到)执行

1
2
3
4
5
6
NSLog(@"begin");
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^{
NSLog(@"end");
});

虽然可以提供NStimer 的功能,但它不是,看下面官方文档:

1
Enqueue a block for execution at the specified time.

Enqueue,就是入队,指的就是将一个Block在特定的延时以后,加入到指定的队列中,不是在特定的时间后立即运行!。这里有个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"begin");
dispatch_async(queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
[NSThread sleepForTimeInterval:10];
NSLog(@"dispatch_group_async");
});
//1.使用 dispatch_after 延后工作
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, queue, ^{
NSLog(@"dispatch_time");
});

log:

1
2
3
2016-09-23 22:56:05.258 GCDTest[20684:11209748] begin
2016-09-23 22:56:15.263 GCDTest[20684:11209796] dispatch_group_async
2016-09-23 22:56:15.263 GCDTest[20684:11209796] dispatch_time

从结果也验证了,dispatch_after只是延时提交block,并不是延时后立即执行。所以想用dispatch_after精确控制运行状态的朋友可要注意了~

dispatch_apply

dispatch_apply的作用是在一个队列(串行或并行)上“运行”多次block,不过会阻塞当前线程,按执行最长任务时间阻塞(异步,同步是所有任务时间总和),用时间要小心:

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"apply 之前");
dispatch_apply(3, queue, ^(size_t y) {
switch (y) {
case 0:[NSThread sleepForTimeInterval:1];break;
case 1:[NSThread sleepForTimeInterval:2];break;
case 2:[NSThread sleepForTimeInterval:3];break;
default:break;
}
NSLog(@"%ld",y);
});
NSLog(@"apply 之后");

log:

1
2
3
4
5
2016-09-23 23:30:24.984 GCDTest[20779:11373252] apply 之前
2016-09-23 23:30:25.985 GCDTest[20779:11373252] 0
2016-09-23 23:30:26.987 GCDTest[20779:11373585] 1
2016-09-23 23:30:27.988 GCDTest[20779:11373584] 2
2016-09-23 23:30:27.988 GCDTest[20779:11373252] apply 之后

将其放在异步线程去执行,这样就不阻塞主线程了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSLog(@"dispatch_async 之前");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//使用dispatch_apply可以运行的更快
NSLog(@"apply 之前");
dispatch_apply(3, queue, ^(size_t y) {
switch (y) {
case 0:[NSThread sleepForTimeInterval:1];break;
case 1:[NSThread sleepForTimeInterval:2];break;
case 2:[NSThread sleepForTimeInterval:3];break;
default:break;
}
NSLog(@"%ld",y);
});
NSLog(@"apply 之后");
});
NSLog(@"dispatch_async 之后");

log:

1
2
3
4
5
6
7
2016-09-23 23:38:21.221 GCDTest[20817:11403936] dispatch_async 之前
2016-09-23 23:38:21.222 GCDTest[20817:11403936] dispatch_async 之后
2016-09-23 23:38:21.222 GCDTest[20817:11404070] apply 之前
2016-09-23 23:38:22.223 GCDTest[20817:11404070] 0
2016-09-23 23:38:23.226 GCDTest[20817:11404072] 1
2016-09-23 23:38:24.225 GCDTest[20817:11404069] 2
2016-09-23 23:38:24.226 GCDTest[20817:11404070] apply 之后
dispatch_barrier_async

dispatch_barrier_async的作用就是向某个队列插入一个block,当目前正在执行的block运行完成后,阻塞这个block后面添加的block,只运行这个block直到完成,然后再继续后续的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dispatch_group_t group = dispatch_group_create();
// dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog(@"group-01 - %@", [NSThread currentThread]);
});


dispatch_barrier_async(queue, ^{
NSLog(@"barrier - %@", [NSThread currentThread]);
});



dispatch_group_async(group, queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"group-02 - %@", [NSThread currentThread]);
}
});


dispatch_group_async(group, queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});

log:

1
2
3
4
5
6
7
8
2016-09-23 23:54:08.478 GCDTest[20928:11478999] group-01 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.478 GCDTest[20928:11478999] barrier - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.480 GCDTest[20928:11478999] group-02 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.480 GCDTest[20928:11478997] group-03 - <NSThread: 0x60000026c680>{number = 4, name = (null)}
2016-09-23 23:54:08.481 GCDTest[20928:11478999] group-02 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.481 GCDTest[20928:11478997] group-03 - <NSThread: 0x60000026c680>{number = 4, name = (null)}
2016-09-23 23:54:08.482 GCDTest[20928:11478999] group-02 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.482 GCDTest[20928:11478997] group-03 - <NSThread: 0x60000026c680>{number = 4, name = (null)}
  • dispatchbarrier\(a)sync 只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
  • 既然在串行队列上跟dispatch_(a)sync效果一样,那就要小心别死锁!

在dispatch_get_global_queue:

1
2
3
4
5
6
7
8
2016-09-23 23:56:14.201 GCDTest[20946:11496633] group-01 - <NSThread: 0x608000263780>{number = 3, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497029] group-02 - <NSThread: 0x600000277200>{number = 5, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497030] group-03 - <NSThread: 0x608000265840>{number = 6, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497028] barrier - <NSThread: 0x608000265680>{number = 4, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497029] group-02 - <NSThread: 0x600000277200>{number = 5, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497030] group-03 - <NSThread: 0x608000265840>{number = 6, name = (null)}
2016-09-23 23:56:14.202 GCDTest[20946:11497029] group-02 - <NSThread: 0x600000277200>{number = 5, name = (null)}
2016-09-23 23:56:14.202 GCDTest[20946:11497030] group-03 - <NSThread: 0x608000265840>{number = 6, name = (null)}
暂停与恢复
  • dispatch_suspend(queue); 暂停队列
  • dispatch_resume(queue); 恢复队列

dispatch_suspend函数对已经执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而dispatch_resume则使得这些处理能够继续执行。

这里有个例子(参考来自这里

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
dispatch_queue_t queue1 = dispatch_queue_create("com.queue1", 0);
dispatch_queue_t queue2 = dispatch_queue_create("com.queue2", 0);
dispatch_group_t group = dispatch_group_create();
dispatch_async(queue1, ^{
NSLog(@"任务 1 : queue 1...");
sleep(1);
NSLog(@"✅完成任务 1");
});


dispatch_async(queue2, ^{
NSLog(@"任务 1 : queue 2...");
sleep(1);
NSLog(@"✅完成任务 2");
});



dispatch_group_async(group, queue1, ^{
NSLog(@"🚫正在暂停 1");
dispatch_suspend(queue1);
});

dispatch_group_async(group, queue2, ^{
NSLog(@"🚫正在暂停 2");
dispatch_suspend(queue2);
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"=======等待两个queue完成, 再往下进行...");
dispatch_async(queue1, ^{
NSLog(@"任务 2 : queue 1");
});

dispatch_async(queue2, ^{
NSLog(@"任务 2 : queue 2");
});

NSLog(@"🔴为什么这个NSLog会在上面两个NSLog之前打印❓❓答:dispatch_suspend的作用‼️");

dispatch_resume(queue1);
dispatch_resume(queue2);

log:

1
2
3
4
5
6
7
8
9
10
2016-09-24 22:12:58.646 GCDTest[22178:12407680] 任务 1queue 1...
2016-09-24 22:12:58.646 GCDTest[22178:12408707] 任务 1queue 2...
2016-09-24 22:12:59.650 GCDTest[22178:12407680] ✅完成任务 1
2016-09-24 22:12:59.650 GCDTest[22178:12408707] ✅完成任务 2
2016-09-24 22:12:59.650 GCDTest[22178:12407680] 🚫正在暂停 1
2016-09-24 22:12:59.650 GCDTest[22178:12408707] 🚫正在暂停 2
2016-09-24 22:12:59.650 GCDTest[22178:12407286] =======等待两个queue完成, 再往下进行...
2016-09-24 22:12:59.651 GCDTest[22178:12407286] 🔴为什么这个NSLog会在上面两个NSLog之前打印❓❓答:dispatch_suspend的作用‼️
2016-09-24 22:12:59.651 GCDTest[22178:12408707] 任务 2queue 1
2016-09-24 22:12:59.651 GCDTest[22178:12407680] 任务 2queue 2

Dispatch Source (信号源)

GCD中除了主要的 Dispatch Queue 外,还有不太引人注目的 Dispatch Source .它是BSD系内核惯有功能kqueue的包装。kqueue 是在 XNU 内核中发生各种事件时,在应用程序编程方执行处理的技术。其 CPU 负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理 XNU 内核中发生的各种事件的方法中最优秀的一种。

Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。

Dispatch Source 可处理的所有事件。如下表所示:

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 监测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

其中DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_DATA_OR是常用的两个,其它用于Mac开发的比较多。

在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )。

这个过程叫 Custom event ,用户事件。是 dispatch 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
28
29
 dispatch_source_t queueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
__block NSUInteger totalComplete = 0;
dispatch_source_set_event_handler(queueSource, ^{
//当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。
//这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
NSUInteger value = dispatch_source_get_data(queueSource);
totalComplete += value;
NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
});
/*
分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
因为忘记恢复分派源的状态而产生bug是常见的事儿。
恢复的方法是调用 dispatch_resume :
*/

dispatch_resume(queueSource);

/*
在每次循环中执行加1操作。也可以传递已处理记录的数目或已写入的字节数。
在任何线程中都可以调用 dispatch_source_merge_data 。
需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数(会无穷大)。
*/

dispatch_queue_t queue = dispatch_queue_create("com.sourcequeue", DISPATCH_QUEUE_CONCURRENT);
for (NSUInteger index = 0; index < 10; index++) {
dispatch_async(queue, ^{
dispatch_source_merge_data(queueSource, 1);
[NSThread sleepForTimeInterval:1];
});
}
NSLog(@"queue for 循环 完成");

log:

1
2
3
4
5
6
2016-09-25 09:35:20.128 GCDTest[22525:12717227] queue for 循环 完成
2016-09-25 09:35:20.129 GCDTest[22525:12718440] 进度:0.3
2016-09-25 09:35:20.129 GCDTest[22525:12718440] 进度:0.4
2016-09-25 09:35:20.129 GCDTest[22525:12718440] 进度:0.6
2016-09-25 09:35:20.130 GCDTest[22525:12718440] 进度:0.9
2016-09-25 09:35:20.130 GCDTest[22525:12718440] 进度:1

可以看到 0.1~1 并没有全部打印,而是跳过:

这是 DispatchSource 能通过合并事件的方式确保在高负载下正常工作 的能力

在同一时间,只有一个处理 block 的实例被分配,如果这个处理方法还没有执行完毕,另一个事件就发生了,事件会以指定方式(ADD或 OR)进行累积。DispatchSource能通过合并事件(block)的方式确保在高负载下正常工作。当处理事件被最终执行时,计算后的数据可以通过 dispatch_source_get_data 来获取。这个数据的值在每次响应时间执行后会被重置,所以上面的例子中进度条 totalComplete 的值是最终积累的值,而 block 不是每次都执行的。但能确保进度条能从0.0到1.0的正常执行。

Dispatch Semaphore 信号量

为了展示作用,举个反例:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_group_t group = dispatch_group_create();
for(int i = 0; i< 100000; ++i) {
dispatch_group_async(group, queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"%@", @([array count]));

然后就发生了崩溃:

1
2
malloc: *** error for object 0x7f88ed03ca00: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug

这种资源抢夺的情况,一般的做法是使用串行队列,或者像下面一样的同步队列,得以解决:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_group_t group = dispatch_group_create();
for(int i = 0; i< 1000; ++i) {
dispatch_sync(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"%@", @([array count]));

dispatch_semaphore_t 的作用之一就是解决这种资源抢夺的情况,下面展示下展示使用 dispatch_semaphore_t 的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_queue_t queue = dispatch_queue_create("com.semaphore", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"🔴%@ index = %d",[NSThread currentThread],i);
[array addObject:@(i)];
dispatch_semaphore_signal(semaphore);
});
}
dispatch_barrier_async(queue, ^{
NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @([array count]));
});

测试,设置:

1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

可以发现 程序会永远停留在:

1
2
3
等待Dispatch Semaphore
一直等待,直到Dispatch Semaphore的计数值达到大于等于1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

关于信号量这里有非常详细的解释

dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是

dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

下面我们逐一介绍三个函数:

①dispatch_semaphore_create的声明为:

dispatch_semaphore_t  dispatch_semaphore_create(long value);

传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。

值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

(关于信号量,我就不在这里累述了,网上很多介绍这个的。我们这里主要讲一下dispatch_semaphore这三个函数的用法)。

②dispatch_semaphore_signal的声明为:

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

这个函数会使传入的信号量dsema的值加1;(至于返回值,待会儿再讲)
dispatch_semaphore_signal的返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理

的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一

个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。

dispatch_semaphore_wait的返回值也为long型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒。

当其返回不为0时,表示timeout发生。

③dispatch_semaphore_wait的声明为:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

这个函数会使传入的信号量dsema的值减1;

这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;

如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,

不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,

且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。

如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

关于信号量,这里有一个很形象的解释:

1
2
3
4
5
6
7
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),
调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;
当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主
没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,
所以就一直等下去。

本文到此就结束了


参考:

Parse源码浅析系列(一)—Parse的底层多线程处理思路:GCD高级用法

关于dispatch_semaphore的使用

GCD使用经验与技巧浅谈

iOS 并发编程之 Operation Queues

关于iOS多线程,你看我就够了

小笨狼漫谈多线程:NSThread