1.队列
最大的分类有两种:串行和并行,对应的创建方法如下
// 串行 dispatch_queue_t synQueue = dispatch_queue_create("syn", DISPATCH_QUEUE_SERIAL); // 并行 dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);
上面的这种创建方式是自定义队列,第一个参数是队列标识,第二个参数是指定是哪种队列。
系统有本身的队列,比如主队列和全局队列
// 主队列 dispatch_get_main_queue(); // 全局队列 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主队列是串行队列,这也是为什么我们网络请求一般不在主队列发起,因为这样在请求返回前会阻塞当前队列,无法进行其他操作;全局队列是并行队列,第一个参数是优先级,第二个参数是给苹果预留的,一般为0或NULL。
所以一般面试问GCD有几种队列,一般是主队列、全局队列和自定义队列。
2.执行方法
串行执行方法是
dispatch_sync(syncQueue, ^{});
并行执行方法是
dispatch_async(asyncQueue, ^{});
上面的写法是,串行队列调用串行方法,并行队列调用并行方法,那如果交叉过来,串行队列调用并行方法,并行队列调用串行方法,执行顺序会是如何呢?如果一个队列先后调用串行和并行方法,又是怎样执行的?
测试代码和过程我就不写了,这里先写自己测试后的结论:
串行队列执行并行方法,下一个任务会等上一个任务完全完成后才开始执行;并行队列执行串行方法,是按顺序执行。
3.barrier(屏障、栅栏)
并行队列并行执行的时候,所有任务都是随时执行的,但如果其中一个任务很重要,需求中它将会影响后面队列中的任务,该如何实现?GCD中有一个功能,这个功能类似于创建一个屏障、栅栏区域来隔开前后的任务,前面的任务执行完后,再执行栅栏区域的任务,才能执行后面的任务。
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_async(asyncQueue, ^{ NSLog(@"1"); }); dispatch_async(asyncQueue, ^{ NSLog(@"2"); }); dispatch_barrier_async(asyncQueue, ^{ NSLog(@"barrier"); }); dispatch_async(asyncQueue, ^{ NSLog(@"3"); }); dispatch_async(asyncQueue, ^{ NSLog(@"4"); });
输出结果是1,2,barrier,3,4,也有可能会是2,1,barrier,4,3,但barrier一定会在前两个任务执行完之后,后两个任务执行之前执行的。
4.GCD和线程同步
如果多个线程访问同一处代码,那么可能会出现问题,比如对同一个值的get和set方法,多线程的时候,由于执行的时机是随时的,所以我们有可能访问get方法获取到的不是最新的值。通常是使用锁来实现同步机制,比较常用是@synchronized或者NSLock及其子类来加锁。
- (void)lockMethod { @synchronized(self) { //do something } }
以上这种写法是根据给的对象,自动创建一个锁,等到block总的代码执行完,就释放了锁。但是以上的这个例子代码里,由于锁的对象是self,@synchronized的作用是保证此时没有其他线程对self对象进行访问,这样如果我们在访问加锁程序的同时,就不能访问其他无关的代码了,所以滥用@synchronized会降低代码效率。
NSLock及其子类与@synchronized有一种缺陷,在极其极端的情况下,同步块会导致死锁,另外,效率也不见得很高。
替代方案是使用GCD,示例代码如下:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); - (NSString *)stringA { __block NSString* A; dispatch_async(asyncQueue, ^{ A = _localA; }); return A; } - (void)setStringA:(NSString *)A { dispatch_barrier_async(asyncQueue, ^{ _localA = A; }); }
上面的示例里,所有的读取A的操作,都被barrier屏蔽住,必须等赋值完后才能读取,此时读取到的A是最新的值。
5.任务组group
GCD可以把任务分组,调用者会在回调函数中收到一组任务结束的通知。
任务组的创建:
dispatch_group_t group = dispatch_group_create();
通常的调用方法:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, asyncQueue, ^{ NSLog(@"1"); }); dispatch_group_async(group, asyncQueue, ^{ NSLog(@"2"); }); dispatch_group_async(group, asyncQueue, ^{ NSLog(@"3"); }); dispatch_group_notify(group, asyncQueue, ^{ NSLog(@"end"); });
等效于下面的这种方式,但区别是在任务完成前会阻塞:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); dispatch_async(asyncQueue, ^{ NSLog(@"1"); dispatch_group_leave(group); }); dispatch_group_enter(group); dispatch_async(asyncQueue, ^{ NSLog(@"2"); dispatch_group_leave(group); }); dispatch_group_enter(group); dispatch_async(asyncQueue, ^{ NSLog(@"3"); dispatch_group_leave(group); }); dispatch_group_wait(group, DISPATCH_TIME_FOREVER); NSLog(@"end");
dispatch_group_enter(group)和dispatch_group_leave(group)必须成对出现,表示一个任务进入组、离开组,类似于引用计数的retain和release。dispatch_group_wait是用来阻塞线程的,第二个参数是时间参数,表示要阻塞多久,我们用DISPATCH_TIME_FOREVER表明一直等,等到任务组都执行完才能向下执行其他的。如果在调用enter之后,没有对应的leave,那么这一任务永远执行不完,会由dispatch_group_wait一直阻塞着。
思考:如果是一个任务组,执行两个并行队列的所有任务,会是如何?
6.循环执行
如果我们碰上需要循环执行某些任务,比如遍历一个数组做操作,又不想阻塞线程,该如何做呢?
用for循环这么做:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); for (id obj in array) { dispatch_async(asyncQueue, ^{ // do something }); }
但如果再加个需求,在遍历操作执行完后,才执行下一个任务,显然上面的这个方法不合适,用串行又显得不够好。现在我们开业参考下上面的group来实现:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); for (id obj in array) { dispatch_group_async(group, asyncQueue, ^{ // do something }); dispatch_group_notify(group, asyncQueue, ^{ // after end do something }); }
幸好,对于循环,GCD提供了一个dispatch_apply函数来实现:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_apply(10, asyncQueue, ^(size_t index) { NSLog(@"index %ld", index); }); NSLog(@"end");
但是dispatch_apply是同步方法,会阻塞线程,于是对上面的代码做一下调整:
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_async(asyncQueue, ^(){ dispatch_apply(10, asyncQueue, ^(size_t index){ NSLog(@"index %ld", index); }); NSLog(@"end"); });
7.单例
+ (id)shareInstance { static MyClass *myShareInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ myShareInstance = [[MyClass alloc] init]; }); return myShareInstance; }
8.延时delay
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (5.0 * NSEC_PER_SEC)); dispatch_after(time, dispatch_get_main_queue(), ^{ // do something });
示例代码是延迟5秒。GCD的延迟方法比起performSelector的延迟方法好处是,代码集中不分散,不用另外在写执行方法,而且传参数没有限制,performSelector的参数必须是id类型。
9.优先级
调用全局队列的时候,有个参数是优先级,但一般是默认优先级DISPATCH_QUEUE_PRIORITY_DEFAULT,一共有四个优先级,按顺序是:高、默认、低、后台。高优先级会先执行,但注意,在极端的情况下会出现优先级反转的情况,低优先级的任务占有资源导致高优先级任务无法执行。
10.信号量
对于多个线程访问同个资源,GCD还提供是一种解决方法,就是信号量dispatch_semaphore
dispatch_queue_t asynQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT); dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); for (int i = 0; i < 20; i++) { dispatch_async(asynQueue, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"semaphore %@------ %i", semaphore, i); sleep(2); dispatch_semaphore_signal(semaphore); }); }
dispatch_semaphore_create(2)创建了一个总量为2的信号量;
dispatch_semaphore_wait是等待信号,并让信号量-1,如果获取到的信号量是0,那么根据设置的超时时间进行等待,例子里设置的超时时间是一直;
dispatch_semaphore_signal是发送信号,并让信号量+1;
这套信号机制是不是很类似引用计数。所以,上面的这段代码并发了20个任务,每个任务都会有sleep,但每执行2个任务,经由dispatch_semaphore_wait减了两次,就为0,其他的任务只能等sleep后dispatch_semaphore_signal加回信号量才能执行,如此反复。
最后一些注意
执行异步方法时,是需要拷贝block的,所以若拷贝所需要的时间超过执行的时间,显得效率降低,异步的效果得不偿失。
不要调用dispatch_get_current_queue(),因为如果当前队列正在执行同步方法,会引起死锁。所幸这个方法苹果已经废弃了。