GCD

GCD - Grand Central Dispatch

GCD是异步执行任务的技术之一,能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能;而且,提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。同时具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。

Features

Dispatch Queue

  Dispatch Queue的种类 说明
串行 Serial Dispatch Queue 等待现在执行中处理结束
并行 Concurrent Dispatch Queue 不等待现在执行中处理结束

Dispatch_Queue_Create

dispatch_queue_t serial = dispatch_queue_create("com.clgchao.io",
		 DISPATCH_QUEUE_SERIAL); //串行队列,默认
dispatch_queue_t concurrent = dispatch_queue_create("com.clgchao.io",
    	 DISPATCH_QUEUE_CONCURRENT); //并行队列

第一个参数为名字,推荐使用程序ID或逆序全程域名的。

Main Dispatch Queue/Global Dispatch Queue

Main Dispatch Queue是在主线程执行的,为串行队列。获取方式为:

dispatch_queue_t mainQueue = dispatch_get_main_queue();

Global Dispatch Queue为全局并发队列,分为高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。获取方法为:

dispatch_queue_t globalHigh =
	 	dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t globalDefault =
     	dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t globalLow =
     	dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t globalBackground =
     	dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue

改变queue的优先级与目标queue相同。

dispatch_set_target_queue(Q1,Q2); Q1为需要修改优先级的,Q2为Q1要与他同级的。 MainDQ 与GlobalDQ因为是全局,所以无法作为第一个参数。

在必须将不可并行的处理追加到多个串行队列中时,如果使用dispatch_set_target_queue函数将目标指定为某一个为串行队列即可防止处理并行执行。

dispatch_after 与 dispatch_walltime

dispatch_after 为延迟多久加入队列(相对时间)
dispatch_walltime 为在什么时间加入队列(绝对时间)

使用:

// 延迟3秒后提交
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 
										3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
    	NSLog(@"dispatch_after");
    });

强调:

dispatch_after是延迟提交,不是延迟运行,指的就是将一个Block在特定的延时以后,加入到指定的队列中,不是在特定的时间后立即运行!

注意dispatch_time_t:

dispatch_time_t dispatch_time ( dispatch_time_t when, 
									int64_t delta );

第一个参数一般是 DISPATCH_TIME_NOW ,表示从现在开始。 那么第二个参数就是真正的延时的具体时间。

这里要特别注意的是,delta参数是“纳秒!”,就是说,延时1秒的话,delta应该是“1000000000” (´・ω・`) 太长了,所以理所当然系统提供了常量,如下:

#define NSEC_PER_SEC 1000000000ull
#define NSEC_PER_MSEC 1000000ull
#define USEC_PER_SEC 1000000ull
#define NSEC_PER_USEC 1000ull

关键词解释:

  • NSEC:纳秒。
  • USEC:微妙。
  • SEC:秒
  • MSEC:兆秒
  • PER:每

所以:

  1. NSEC_PER_SEC,每秒有多少纳秒。
  2. NSEC_PER_MSEC,每兆秒有多少纳秒。
  3. USEC_PER_SEC,每秒有多少毫秒。(注意是指在纳秒的基础上)
  4. NSEC_PER_USEC,每毫秒有多少纳秒。

所以,延时 1秒 可以写成如下形式:

dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC);
dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC);

Dispatch Group

在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理的情形,除了使用串行队列外, 在使用并行队列或同时使用多个并行队列的时候,应该使用Dispatch Group。

dispatch_queue_t queue =
	  		dispatch_get_global_queue(DISPATCH_QUEUE_DEFAULT,0);
dispatch_group_t group = disptach_group_create();
dispatch_group_async(group,queue,^{1...});
dispatch_group_async(group,queue,^{2...});
dispatch_group_async(group,queue,^{3...});
dispatch_group_notify(group,dispatch_get_main_queue(),^{End Action});

注意:在dispatch_group_notify函数中不管指定怎么样的Dispatch Queue,属于Dispatch Group的处理在追加到block(上面例子的【End Action】)时都已经结束。

所以,对于添加的block是异步请求时,dispatch_group_async在瞬间就完成了,并不会等待着异步请求结束后再notify。对于这种问题,dispatch_group也有对应的方法:

dispatch_group_enter & dispatch_group_leave这对组合。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = 
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"11");
dispatch_group_enter(group);
[Manager executeWeatherWithCity:@"北京" resultBlock:^(NSString *weather) { // 自定义异步线程
    NSLog(@"22 == %@", weather);
    dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[Manager executeWeatherWithCity:@"北京" resultBlock:^(NSString *weather) { // 自定义异步线程
    NSLog(@"33 == %@", weather);
    dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[Manager executeWeatherWithCity:@"北京" resultBlock:^(NSString *weather) { // 自定义异步线程
    NSLog(@"44 == %@", weather); 
    dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[Manager executeWeatherWithCity:@"北京" resultBlock:^(NSString *weather) { // 自定义异步线程
    NSLog(@"55 == %@", weather);
    dispatch_group_leave(group);
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"66");
    [Manager executeWeatherWithCity:@"北京" resultBlock:^(NSString *weather) {
        NSLog(@"77 == %@", weather);
    }];
});
NSLog(@"88");

执行顺序就是先 1和8,之后2到5随机,最后才是66以及77。

dispatch_group_enter() 和 dispatch_group_leave()必须成对出现,group_enter是将请求任务放入到group后,便一直被group持有,直到碰到group_leave;才会释放出来,只有group中不在持有任何任务后才会调用notify进行回调通知。

dispatch_group_wait

dispatch_group_wait(group,DISPATCH_TIME_FOREVER);

该函数的第二个参数指定超时等待的时间,为dispatch_time_t类型。该源代码使用 DISPATCH_TIME_FOREVER,意味着永久等待。只有属于Dispatch Group的处理尚未结束,就回一直等待,中途不能取消。

disptach_time_t time = dispatch_time(DISPATCH_TIME_NOW,1ull*NSEC_PER_SEC);
long result = dispatch_group_wait(group,time);

//返回值为long型
if(result == 0){
    // 等待时间结束后,group的全部处理执行结束
}else{
    // 等待时间结束后,group的还有未完成的处理
}

修改上述源代码result中的时间,不用等待即可判断属于Dispatch Group的处理是否执行完毕。

long result = dispatch_group_wait(group,DISPATCH_TIME_NOW);

正常情况下不使用dispatch_group_wait,而使用dispatch_group_notify, 可以更好的简化代码。

dispatch_barrier_async

在访问数据库或文件时,使用串行队列可避免数据竞争问题。

但为了高效的进行访问,读取处理追加到并行队列中使用并发读取数据,就容易引起问题。

queue 为并发队列
queue 其他处理1
queue 其他处理2
dispatch_barrier_async(queue,blk_for_writing)
queue 其他处理3
queue 其他处理4

dispatch_barrier_async函数会等待追加到并发队列上的并行执行的处理全部结束之后,再将指定的处理追加到该队列中。然后再由dispatch_barrier_async函数追加的处理执行完毕后,队列才恢复为一般的动作,追加到该队列的处理又开始并行执行。

上面的执行顺序即为:

处理1 2 执行完 才会执行dispatch_barrier_async,dispatch_barrier_async执行完才会执行 其他处理 3 4。

如果其他处理都是数据库读操作dispatch_barrier_async是数据库写操作,那么这样能保证 3 4 读取的数据是修改(写入)后的,数据保持正确。

值得注意的是;

  1. dispatch_barrier(a)sync只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
  2. 既然在串行队列上跟dispatch_(a)sync效果一样,那就要小心别死锁!

dispatch_sync

同步执行,与dispatch_async相反。dispatch_sync 函数不会立即返回,会立即阻塞调用时该函数所在的线程,追加任务到添加的线程中,并等待 block同步执行完成。

此方法不能将block加到当前为DISPATCH_QUEUE_SERIAL的线程,会导致死锁。

主线程为DISPATCH_QUEUE_SERIAL的线程,所以也不能直接加。

加入到DISPATCH_QUEUE_CONCURRENT的线程没有任何问题。

注意通过dispatch_get_global_queue获得的线程为DISPATCH_QUEUE_CONCURRENT,所以没问题。

如下两种方式都将死锁。

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"dispatch_sync");
});
    
// 注:后面类型不写的话默认为DISPATCH_QUEUE_SERIAL
dispatch_queue_t queue = 
dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); 
dispatch_async(queue, ^{
    NSLog(@"1");
    dispatch_sync(queue, ^{
        NSLog(@"2");
    });
});

像下面这种情况就不会造成死锁:

dispatch_queue_t queue = 
dispatch_queue_create("serialQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"1");
    dispatch_sync(queue, ^{
        NSLog(@"2");
    });
});
dispatch_queue_t t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(t, ^{
    NSLog(@"3");
    dispatch_sync(t, ^{
        NSLog(@"4");
    });
});

dispatch_apply

按照指定的次数将指定的Block追加到指定的队列中,并等待全部处理执行结束

NSArray *array = @[@"6",@"5",@"4",@"3",@"2",@"1"];
dispatch_apply(array.count, dispatch_get_global_queue(
	DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
    	NSLog(@"%zu---%@", index, [array objectAtIndex:index]);
});
NSLog(@"done");

done 总是最后输出。

要避免dispatch_apply的嵌套调用,会发生死锁。

dispatch_queue_t queue =
	 dispatch_queue_create("com.dechao.gcd", DISPATCH_QUEUE_SERIAL);
       
dispatch_apply(3, queue, ^(size_t i) {
	NSLog(@"apply loop: %zu", i);
   
	//再来一个dispatch_apply!死锁!      
	dispatch_apply(3, queue, ^(size_t j) {
		NSLog(@"apply loop inside %zu", j);
	});
});

dispatch_suspend/dispatch_resume

当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。

dispatch_suspend 挂起已经追加的处理

dispatch_resume 恢复已经追加的处理

dispatch_suspend并不会立即暂停正在运行的block,而是在当前block执行完成后,暂停后续的block执行。

所以下次想暂停正在队列上运行的block时,还是不要用dispatch_suspend了吧~

Dispatch Semaphore

信号量是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时,线程会被阻塞(如果有必要的话),直至计数器大于零,然后线程会减少这个计数。

在GCD中有三个函数是semaphore的操作,分别是:

dispatch_semaphore_create   创建一个semaphore

dispatch_semaphore_signal   发送一个信号

dispatch_semaphore_wait    等待信号

简单的介绍一下这三个函数,第一个函数有一个整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,根据这样的原理,我们便可以快速的创建一个并发控制来同步任务和有限资源访问控制。

注意,正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。

举例如下:

dispatch_group_t  group = dispatch_group_create();
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++)
{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_group_async(group, queue, ^{
        NSLog(@"%i",i);
        sleep(2);
        dispatch_semaphore_signal(semaphore);
    });
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

简单的介绍一下这一段代码,创建了一个初使值为10的semaphore,每一次for循环都会创建一个新的线程,线程结束的时候会发送一个信号,线程创建之前会信号等待,所以当同时创建了10个线程之后,for循环就会阻塞,等待有线程结束之后会增加一个信号才继续执行,如此就形成了对并发的控制,如上就是一个并发数为10的一个线程队列。

Dispatch Once

说明:Executes a block object once and only once for the lifetime of an application.

保证在应用程序执行中只执行一次指定的API。

常用来声明单例,即使在多线程的环境下,也可以保证安全。

dispatch_once_t必须是全局或static变量,毕竟非全局或非static的dispatch_once_t变量在使用时会导致非常不好排查的bug,正确的如下:

//静态变量,保证只有一份实例,才能确保只执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	//单例代码 
});

其实就是保证dispatch_once_t只有一份实例。

Dispatch I/O

在读取大文件时,如果将文件分为合适的大小并使用Global Dispatch Queue并行读取数据,读取速度会快不少。现在的输入/输出硬件已经可以做到一次性使用多个线程更快的并列读取,实现这一功能的就是Dispatch I/O和Dispatch Data。 通过使用Dispatch I/O可以并发读写数据,提高效率

Reference

GCD使用经验与技巧浅谈

Grand Central Dispatch (GCD) Reference

GCD 深入理解:第一部分

GCD 深入理解:第二部分

Grand Central Dispatch In-Depth: Part 1/2

Grand Central Dispatch In-Depth: Part 2/2