目录
前言
GCD全称Grand Central Dispatch,是异步执行任务的技术之一。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue(调度队列)中,GCD就能生成必要的线程成并执行任务。
先来看一段实际开发中常常出现的代码。
// (1)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// (2)
dispatch_async(queue, ^{
// 执行耗时的处理,如读取磁盘文件数据
// ...
// 数据读取完成
// (3)
dispatch_async(dispatch_get_main_queue(), ^{
// 执行只能在主线程进行的处理,如更新用户界面
});
});
代码(1)获取了一个调度队列queue,代码(2)将闭包追加到调度队列queue中执行,代码(3)获取了主线程的队列,并将闭包追加到主线程队列中执行。
这段代码将耗时任务放在工作线程中异步处理,当耗时任务完成后又切换到主线程去执行更新用户界面的任务,GCD令这个过程得以如此简洁。
Dispatch Queue种类
Dispatch Queue分为两种,如下表。
Dispatch Queue | 说明 | 使用场景举例 |
---|---|---|
Serial Dispatch Queue (串行队列) | 各个任务串行执行,使用一个线程 | 多个线程更新相同资源导致数据竞争时 |
Concurrent Dispatch Queue (并行队列) | 各个任务可并行执行,使用多个线程 [注1] | 没有数据竞争时 |
两种Dispatch Queue在代码中的类型都为dispatch_queue_t
。
[注1]Concurrent Dispatch Queue并行执行的任务数量取决于当前系统的状态。iOS和OS X会基于Dispatch Queue中的任务数、CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并行执行的任务数。
获取Dispatch Queue
有两种途径获得Dispatch Queue,一种是获取系统提供的Dispatch Queue,一种是调用创建接口进行创建。
获取系统提供的Dispatch Queue
系统提供了Main Dispatch Queue、Global Dispatch Queue两种Dispatch Queue供我们使用。
Dispatch Queue | 类型 | 获取函数 | 说明 |
---|---|---|---|
Main Dispatch Queue | Serial Dispatch Queue | dispatch_get_main_queue | 在主线程执行,只有一个 |
Global Dispatch Queue | Concurrent Dispatch Queue | dispatch_get_global_queue | 执行时有高、默认、低、后台4个优先级 |
// 获取主线程队列
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
// 获取一个优先级为默认的全局并行队列
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_get_global_queue
函数第一个参数指定要获取的Concurrent Dispatch Queue的执行优先级,类型为dispatch_queue_priority_t
。第二个参数为预留字段以备将来使用,传0即可。
dispatch_queue_priority_t | 执行优先级 |
---|---|
DISPATCH_QUEUE_PRIORITY_HIGH | 高 (最高优先) |
DISPATCH_QUEUE_PRIORITY_DEFAULT | 默认 |
DISPATCH_QUEUE_PRIORITY_LOW | 低 |
DISPATCH_QUEUE_PRIORITY_BACKGROUND | 后台(最低优先) |
通过dispatch_queue_create函数创建
通过dispatch_queue_create函数,根据不同的参数我们可以创建所有的两种Dispatch Queue。
通过dispatch_queue_create函数创建的Dispatch Queue,不管是哪种类型,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。
关于如何变更创建的Dispatch Queue执行优先级,见后文“dispatch_set_target_queue”。
- 第一个参数指定生成的Dispatch Queue的名称。推荐使用应用程序ID这种逆序全程域名,因为该名称不仅会出现在Xcode和Instruments的调试器中,也会出现在应用crash时产生的CrashLog中。唯一标识我们的Dispatch Queue,以便我们调试程序。
- 第二个参数指定创建的Dispatch Queue类型,为
NULL
时创建Serial Dispatch Queue,为DISPATCH_QUEUE_CONCURRENT
时创建Concurrent Dispatch Queue。 - 返回值即为创建的Dispatch Queue,类型为
dispatch_queue_t
.
1.创建Serial Dispatch Queue
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.mySerialDispatchQueue", NULL);
一旦创建一个Serial Dispatch Queue并追加处理,系统就会对应创建一个线程并使用。如果创建2000个Serial Dispatch Queue,系统就会创建2000个线程。
线程会产生开销,过多使用多线程会引起大量的上下文切换、消耗大量内存,大幅降低系统的响应性能。
2.创建Concurrent Dispatch Queue
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。当处理结束,应当执行的任务数减少时,XNU内核会结束不再需要的线程。XUN内核仅使用Concurrent Dispatch Queue便可完美管理并执行多个任务的线程。
好消息是 iOS 6.0 或 Mac OS X 10.8以上的版本,ARC已经将GCD对象纳入其管理范围,否则我们仍需手动调用dispatch_release/dispatch_retain去管理我们创建的GCD对象。
另外,Block被追加到Dispatch Queue时,会持有这个Dispatch Queue。
将任务追加到Dispatch Queue
dispatch_async(异步追加)
async即异步,将指定的Block“异步”追加到指定的Dispatch Queue中,dispatch_async函数不做任何等待。
如下代码将会先输出“flag B”再输出“flag A”。
dispatch_async(queue, ^{
NSLog(@"flag A");
});
NSLog(@"flag B");
dispatch_sync(同步追加)
sync即同步,将指定的Block“同步”追加到指定的Dispatch Queue中。在追加的Block执行结束之前,dispatch_sync函数会一直等待。
如下代码将会先输出“flag A”再输出“flag B”。
dispatch_sync(queue, ^{
NSLog(@"flag A");
});
NSLog(@"flag B");
注意:dispatch_sync使用要小心,稍有不慎就会导致程序死锁,同时也绝不应该在主线程使用dispatch_sync。
一个死锁的例子:
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.mySerialDispatchQueue", NULL);
dispatch_sync(queue, ^{
dispatch_sync(queue, ^{ NSLog(@"do something."); });
});
dispatch_after(延时异步追加)
dispatch_after函数在指定时间后将Block“异步”追加到Dispatch Queue中,等同于在指定时间后调用dispatch_async函数。
参数类型 | 说明 | |
---|---|---|
参数1 | dispatch_time_t | 指定追加时间,可用dispatch_time函数或dispatch_walltime函数创建dispatch_time_t |
参数2 | dispatch_queue_t | 指定要追加处理Dispatch Queue |
参数2 | dispatch_block_t | 指定要执行处理的Block |
在3秒后将指定Block追加到Main Dispatch Queue中的源代码如下:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds.");
});
因为Main Dispatch Queue在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的RunLoop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或主线程的处理本身有延迟时,这个时间会更长。
关于时间
dispatch_time和dispatch_walltime函数声明:
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
dispatch_time_t dispatch_walltime(const struct timespec *_Nullable when, int64_t delta);
两个函数都返回when
之后delta
纳秒的时间dispatch_time_t。
- dispatch_time函数中
when
类型为dispatch_time_t,常常可由DISPATCH_TIME_NOW
指定,即现在。
// 创建150毫秒后的时间
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 150ull * NSEC_PER_MSEC);
- dispatch_walltime函数中
when
类型为struct timespec
,
_STRUCT_TIMESPEC
{
__darwin_time_t tv_sec;
long tv_nsec;
};
这是一个使用NSDate创建dispatch_time_t的例子:
// 使用绝对时间创建time
dispatch_time_t getDispatchTimeByDate(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;
}
dispatch_apply(同步批量追加)
该函数按指定的次数,将指定的Block追加到指定的Dispatch Queue中,并等待全部任务执行结束。
函数声明:
void dispatch_apply(size_t iterations,
dispatch_queue_t queue, void (^block)(size_t iteration));
第一个参数为重复次数,第二个参数为追加对象的Dispatch Queue,第三个参数为追加的带有参数的Block。Block的iteration参数用来区分这些重复追加的Block。
例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
NSLog(@"done");
这段代码的执行结果为:
1
5
0
8
3
2
9
6
4
7
done
例子中是追加到Concurrent Dispatch Queue中并行执行,因此输出index的顺序不定,但是“done”一定在最后输出,因为dispatch_apply会等待所有处理执行结束。如果queue是Serial Dispatch Queue,那输出结果将会是:
0
1
2
3
4
5
6
7
8
9
done
dispatch_barrier_async(异步分隔追加)
- dispatch_barrier_async函数解决的是这一类场景的问题:该场景使用串行队列可以避免多线程问题、不会出错,但其中一类任务可以并行执行、不需等待。
- dispatch_barrier_async函数使用参数与dispatch_async方法一致,第一个参数为要追加到的Dispatch Queue,第二个参数为追加的Block。
dispatch_barrier_async与通过dispatch_queue_create创建的Concurrent Dispatch Queue一起使用。
以经典的读写问题为例,下述代码应用了dispatch_barrier_async函数来优化任务调度。
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.myConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ NSLog(@"read task 1"); });
dispatch_async(queue, ^{ NSLog(@"read task 2"); });
dispatch_async(queue, ^{ NSLog(@"read task 3"); });
dispatch_barrier_async(queue, ^{
NSLog(@"write task 4 ");
});
dispatch_async(queue, ^{ NSLog(@"read task 5"); });
dispatch_async(queue, ^{ NSLog(@"read task 6"); });
dispatch_async(queue, ^{ NSLog(@"read task 7"); });
读取任务1、2、3并行运行,读取任务1、2、3全部执行完毕后,执行写入任务4,写入任务4执行完毕后,并行执行读取任务5、6、7。
dispatch_barrier_async函数所追加的任务像一条分界线,将任务执行顺序进行了分层。
有dispatch_barrier_async函数,自然也有dispatch_barrier_sync函数,两者作用类似。与dispatch_barrier_async对应于dispatch_async一样,dispatch_barrier_sync对应于dispatch_sync,即同步执行,当前需要等待执行结束。
挂起与恢复Dispatch Queue
dispatch_suspend与dispatch_resume
当大量任务追到到Dispatch Queue时,在任务执行过程中,有时候希望不执行已经被追加的任务。这时可以用dispatch_suspend函数将指定Dispatch Queue挂起,尚未执行的任务将停止执行,直到调用dispatch_resume函数恢复Dispatch Queue的执行。
函数声明:
void dispatch_suspend(dispatch_object_t object);
void dispatch_resume(dispatch_object_t object);
dispatch_set_target_queue
变更创建的Dispatch Queue执行优先级
前文所述,通过dispatch_queue_create函数创建的Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。GCD提供了dispatch_set_target_queue函数,使我们可以变更优先级。
- 第一个参数为我们创建的Dispatch Queue,即要变更优先级的目标,将第二个参数设置为Global Dispatch Queue。最终效果是参数1将使用与参数2相同优先级的线程。
dispatch_queue_t serialQueue = dispatch_queue_create("com.test.gcd.mySerialDispatchQueue", NULL);
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
// 指定serialQueue使用与backgroundQueue相同优先级的线程,即后台优先级的线程
dispatch_set_target_queue(serialQueue, backgroundQueue);
串行多个Serial Dispatch Queue
当我们的任务不得不分散到多个Serial Dispatch Queue中执行,但我们又想这些任务全部串行执行时,可使用dispatch_set_target_queue函数进行设置。
我们知道多个Serial Dispatch Queue之间是并行的,利用dispatch_set_target_queue,我们可以将这些Serial Dispatch Queue指派到同一个Serial Dispatch Queue上执行,以达到所有任务串行的目的。
下面两段代码效果是等同的
dispatch_queue_t targetSerialQueue = dispatch_queue_create("com.test.gcd.targetQueue", NULL);
dispatch_queue_t serialQueue1 = dispatch_queue_create("com.test.gcd.mySerialDispatchQueue1", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("com.test.gcd.mySerialDispatchQueue2", NULL);
dispatch_queue_t serialQueue3 = dispatch_queue_create("com.test.gcd.mySerialDispatchQueue3", NULL);
dispatch_set_target_queue(serialQueue1, targetSerialQueue);
dispatch_set_target_queue(serialQueue2, targetSerialQueue);
dispatch_set_target_queue(serialQueue3, targetSerialQueue);
dispatch_async(serialQueue3, ^{ NSLog(@"task3"); });
dispatch_async(serialQueue2, ^{ NSLog(@"task2"); });
dispatch_async(serialQueue1, ^{ NSLog(@"task1"); });
dispatch_queue_t targetSerialQueue = dispatch_queue_create("com.test.gcd.targetQueue", NULL);
dispatch_async(targetSerialQueue, ^{ NSLog(@"task3"); });
dispatch_async(targetSerialQueue, ^{ NSLog(@"task2"); });
dispatch_async(targetSerialQueue, ^{ NSLog(@"task1"); });
Dispatch Group
Dispatch Group解决的是这一类场景的问题:想在追加到Dispatch Queue中的多个任务全部结束时执行某个任务。
dispatch_group_notify(监听任务组执行结束后追加)
为了解决上述场景,我们可以创建一个Dispatch Group,将任务追加到queue的同时添加到group中,dispatch_group_notify可以监听所有任务是否执行结束,执行结束后将指定任务追加到指定queue中。
// 获取任意两个队列(这里举例可以使用不同的Dispatch Queue)
> dispatch_queue_t queue1 =
> dispatch_queue_create("com.test.gcd.mySerialDispatchQueue", NULL);
> dispatch_queue_t queue2 =
> dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
> // 创建一个Dispatch Group
> dispatch_group_t group = dispatch_group_create();
> // 将任务异步追加到指定队列,并添加到group
> dispatch_group_async(group, queue1, ^{ NSLog(@"task 1"); });
> dispatch_group_async(group, queue1, ^{ NSLog(@"task 2"); });
> dispatch_group_async(group, queue2, ^{ NSLog(@"task 3"); });
> // 当上述添加的所有任务都执行完毕时,此任务将被异步追加到queue2执行
> dispatch_group_notify(group, queue2, ^{ NSLog(@"done"); });
dispatch_group_t
为Dispatch Group的类型,通过dispatch_group_create函数创建。- dispatch_group_async函数与dispatch_async函数相同,将任务异步追加到指定队列。同时dispatch_group_async使指定的Block属于指定的Dispatch Group。
- dispatch_group_notify函数在属于指定的Dispatch Group的任务全部执行结束时,将指定Block追加到指定的Dispatch Queue中。
函数声明:
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
dispatch_group_wait(等待任务组执行结束)
dispatch_group_notify已经很完美了,以防你喜欢等待,GCD也提供了dispatch_group_wait函数,它会等待追加到Dispatch Group的任务全部直接结束。
intptr_t dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
第一个参数为要等待的Dispatch Group,第二个参数为等待的时间。返回值为0代表经过了指定时间,这些任务都已执行结束,不为0代表还有任务在执行。
毕竟不是所有人对等待的忍受能力都一样,所以我们可以根据需要设置等待时间。以防你是大情种,GCD贴心地提供了让你永远等待的能力:将第二个参数设置为
DISPATCH_TIME_FOREVER
你就可以永远等待下去了。
总结
GCD真好用。
dispatch_after永远滴神。
参考资料:
-
Apple官方指南-Dispatch Queues:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html -
《Objective-C高级编程 iOS与OS X多线程和内存管理》