GCD API——Dispatch Queue与Dispatch Group

前言

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 QueueSerial Dispatch Queuedispatch_get_main_queue在主线程执行,只有一个
Global Dispatch QueueConcurrent Dispatch Queuedispatch_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函数。

参数类型说明
参数1dispatch_time_t指定追加时间,可用dispatch_time函数或dispatch_walltime函数创建dispatch_time_t
参数2dispatch_queue_t指定要追加处理Dispatch Queue
参数2dispatch_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多线程和内存管理》

  • 30
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值