该换换刷新·KafkaRefresh
3 GCD的使用
3.1 __block的设计思想及GCD中的ARC机制
在GCD里,有很多block语法。但现在我想突出一个block的特性,或许你知道这个特性,但是,我并不是为了重复。
当了解一个语法的使用,琢磨语法的设计,也会有乐趣。就像下面这个__block语法。先看这么一段让你厌烦的代码:
NSInteger testNum = 100;
void(^myBlock)(NSInteger num) = ^(NSInteger num){
num = 10;
};
myBlock(testNum);
你知道这样并不会改变testNum的值,如果要改变,需要使用__block。
但有个问题,苹果为什么要这样去设计?单纯为了语法去创造语法肯定不是。
我们知道传统的并发编程里,有着这锁那锁,生怕没锁好,又怕锁没开。而block这则可以避开这一麻烦。block包含的数据或代码,不会轻易被其他block改变,这就避免了不停加锁解锁的繁杂。
这样优秀的设计理念在Objective-c中有很多,比如说让category去遵循协议是被允许的,当把category看做一个插件时,让插件去完成某个协议规定的事情,这样的做法会极大的减少ViewController的代码量,让代码看起来也更加清晰可读,提高维护性。
我见过有人在ARC机制下,将GCD的代码放入一个autoreleasepool,在ARC下,GCD的对象都是自动释放的,你不必多虑。但是,可以这样做吗?
3.2 Dispatch Queues(调度队列)
3.2.1 GCD采用队列的设计的优势
在说队列之前,想说一下面向对象的对象的概念,朋友说面向对象里,把所有东西看成对象,这话似乎也没错,只是觉得太唯心了,或者说,说了跟没说一样。而我对对象的理解是这样:在面向对象的编程中,为了将程序逻辑模块化或者部件化,达到简化程序逻辑的目的,那么,这些模块或者部件,可以称之为对象。
Dispatch Queue是一个遵循了OS_dispatch_queue协议的NSObject对象,按照苹果的说法,还是一个轻量级的对象,就是说,一个Dispatch Queue的创建和销毁的系统开销很小。
GCD采用队列的概念相比于传统概念有如下优势:
- 提供了简单直接的编程接口。
- 提供自动和整体线程池管理。
- 提供调谐组件的速度。
- 内存效率更高(因为线程堆栈不会在应用程序内存中停留)。
- 不会在负载下陷入内核引发中断问题。
- 将任务异步分派到队列不会造成队列死锁。
- 串行队列为锁和其他同步技术提供了更有效的替代方案。
3.2.2 Dispatch Queues中的任务
上文说过,任务是对工作的抽象,更具体的说,任务是对某一段代码的整体包装。GCD能够识别的任务只有两种,一个block,另一个是C函数。下面代码总有些不常见的参数,会在接下的文章中逐步讲述。
如何创建一个GCD任务:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// 任务创建最简单的方式
dispatch_block_t task1 = ^{
NSLog(@"这个任务是打印下");
};
task1();
//*************************************************************************************************************
dispatch_block_t task2 = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"这是个阻塞任务");
});
task2();
//*************************************************************************************************************
// 用C函数包装任务
dispatch_function_t func = base_c_task;
func(NULL);
}
void base_c_task(void *arg)
{
NSLog(@"用C函数包装任务");
}
3.2.3 Dispatch Queues的类型、创建与释放
Dispatch Queues有两种类型,DISPATCH_QUEUE_SERIAL与DISPATCH_QUEUE_CONCURRENT,分别代表串行队列与并发队列。见过很多人说起两者的区别是:串行队列里的任务是依次执行,而并发队列的任务是同时执行。这么评价这种说法呢?还是说说自己的看法:不管你创建的队列是串行的还是并发的,在队列里,这些任务都是按照FIFO的原则执行,只是串行队列里的任务需要等待上一个任务完成后才执行下一个任务,而并发队列不等待上一个任务执行完成,而是直接执行下一个任务。所以个人认为“同时”二字不严谨。
/// 分别创建一个串行队列与并发队列
dispatch_queue_t serialQueue = dispatch_queue_create("gs.SERIAL.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrentQueue = dispatch_queue_create("gs.CONCURRENT.com", DISPATCH_QUEUE_CONCURRENT);
参数label是可选的,可以为NULL,苹果使用这个参数的目的其实是为了让开发者自己看到这个队列时,知道这个队列的用途,在程序调试时也比较有用。所以,label的字符串,你就自己描述用途吧。
你不必担心你创建多个队列后,因label参数相同而导致只创建了一个队列,不会的!想获得指定队列的Label参数,通过调用dispatch_queue_get_label(dispatch_queue_t queue)即可。
关于Dispatch Queues的释放,一旦你把一个任务加入到队列里头,那么这个任务保持一个对队列的强引用,任务完成一个,引用计数减去1,当所有任务全部执行完毕后,队列被释放掉。但是,如果任务中创建了多个对象,则可能需要将block的代码的一部分包含在@autorelease中以处理这些对象的内存管理。尽管GCD调度队列具有自己的自动释放池,但它们不能保证何时耗尽这些池。如果应用程序受内存限制,则创建自己的自动释放池允许定期的时间间隔释放自动释放对象的内存。
Queues are not bound to any specific thread of execu-tion execution
tion and blocks submitted to independent queues may execute concurrently.
系统管理的线程池处理提交到队列里的任务,理论上说,每一个队列都有它自己的执行线程,但是队列不会绑定到任何特定的线程。
3.2.4 两个特殊队列
- main queue主队列
main queue是由系统创建的,并且与主线程(main thread)相关联。通过调用dispatch_get_main_queue获得主队列。
iOS应用通过只能通过以下方式中的唯一一种将任务提交到主队列:
1、 调用dispatch_main;
2、 调用UIApplicationMain;
3、 在主线程里使用CFRunLoopRef。
- global queue全局队列
全局队列是由系统定义,一个可以指定任务优先级的队列,当然,这也是一个并发队列。调用dispatch_get_global_queue获得取该队列。
__unused dispatch_queue_t defaultGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
DISPATCH_QUEUE_PRIORITY_DEFAULT这个参数在队列的优先级中会说到。
3.2.5 GCD的同步与异步概念
使用GCD中不需要有线程的概念!
同步处理指定队列中的任务,调用dispatch_sync,异步处理指定队列中的任务,调用dispatch_async。
尽管GCD使系统维护着一个线程池,但根本不需要取关心这些线程池中的线程。iOS应用是在主线程进行人机交互,为了更好的用户体验,我们不希望人机交互出现迟缓,也就是主线程被阻塞。
当我们不想当前的操作等待另外一个操作执行完毕才执行时,我们使用异步的概念去描述这种操作。本质上,为了达到这种效果,GCD使用基于线程的并发,也就是新建线程。但GCD屏蔽了开发者直接访问线程,线程是GCD的内部概念,无需暴露出来。只需要告诉GCD该操作是等待还是不等待。等待则是同步,不等待则是异步。官方文档会这样说:异步执行是立刻返回的,同步执行则等待任务处理完成。返回是什么概念呢,返回就是可以处理下一条指令了。简述这个概念,只是因为朋友问我并发是不是异步执行的问题,让我吃了一惊!
还想继续唠叨这两个概念,要是不懂,所有的都白说。如何用中文翻译如下代码的意思:
dispatch_queue_t queue = dispatch_queue_create("async.com", DISPATCH_QUEUE_SERIAL);
dispatch_block_t task = ^{
NSLog(@"打印的任务 task");
};
// 如何翻译下面这句话?
dispatch_async(queue, task);
不等待队列queue中的任务task执行完毕。
3.2.6 GCD的队列获取
在GCD中没有一个叫做dispatch_get_current_thread()的API,但在iOS 6之前,却有dispatch_get_current_queue(),这个API后来被废弃。在iOS 5的时,GCD更新了一些新的API,让我们去做dispatch_get_current_queue()做的事情。有时,我们可能需要判断该任务是否是在指定的队列里,那么这么做:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
static const void * const kFirstQueueKey = (void *)"FirstQueueKey";
static const void * const kSecondQueueKey = (void *)"SecondQueueKey";
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t secondQueue = dispatch_queue_create("second.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(firstQueue, kFirstQueueKey, (void *)kFirstQueueKey, NULL);
dispatch_queue_set_specific(secondQueue, kSecondQueueKey, (void *)kSecondQueueKey, NULL);
dispatch_sync(firstQueue, ^{
if(dispatch_get_specific(kFirstQueueKey))
{
NSLog(@" 运行在 firstQueue 中");
}
dispatch_sync(secondQueue, ^{
if(dispatch_get_specific(kSecondQueueKey))
{
NSLog(@" 运行在 secondQueue 中");
}
});
});
}
dispatch_queue_set_specific和dispatch_get_specific两个API,跟Runtime里的objc_setAssociatedObject和objc_getAssociatedObject很相似。dispatch_queue_set_specific是给指定队列绑定一个标识,dispatch_get_specific则是获取指定队列的标识,这样,就能判断是哪一个队列。不过,GCD还有其他API可以判定是否为指定队列,是属于GCD罕见的用法。这些会在下文的一次讲解这些不常见的用法。
其实,自己也好奇苹果为何要废弃dispatch_get_current_queue,找不到其他解释,但从runtime的这个API看,这种废弃后更新的API符合OC语言整体的“绑定获取”的设计。当然,这只是个人瞎说罢了,罢了罢了。
3.2.7 GCD的队列上下文数据绑定
上下文数据绑定可以看成是对dispatch_object_t 对象进行真正的数据绑定,虽然极少使用,但还是做一介绍。其中有需要一些注意的地方。
- (void)viewDidAppear:(BOOL)animated
{
/**
创建一个串行队列
*/
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_SERIAL);
/**
1、绑定一个void *的数据类型,但需要__bridge_retained关键字;
2、当你创建一个OC对象进行绑定时,当方法执行完毕,这个对象被释放;
3、要保证在block中获取正确的数据时,需要保证该数据还存在;
4、使用__bridge_retained关键字能避开ARC,这样就能手动释放。
*/
dispatch_set_context(firstQueue, (__bridge_retained void *)@"123456789");
/**
析构函数,在这个函数调用时,释放绑定的数据。这个函数会在指定队列中的所有应用全部被释放后
*/
dispatch_set_finalizer_f(firstQueue, freeContext);
dispatch_sync(firstQueue, ^{
NSLog(@"%@",dispatch_get_context(firstQueue));
NSLog(@" 运行在 firstQueue 中");
});
}
void freeContext(void * ctx)
{
CFRelease(ctx);
}
3.2.8 GCD的队列优先级与线程复用
标题有点诡异,但是,GCD中的队列与队列的联系,很少被用到。有次看到官方文档这样一句话:
By doing that, you can prevent the tasks executing concurrently if you have tasks that shouldn’t be executed concurrently and they must be added to different serial dispatch queues. Actually, I have no idea of such a situation, though.
最后一句话,让我想到,GCD的功能,在某些方面或许被过度设计了,开发者可能永远也用不到。但是,当能把这些情况考虑完全,不失为严谨。
dispatch_queue_create创建的队列,其优先级默认与dispatch_get_global_queue相同。而dispatch_set_target_queue则可以更改队列优先级。先看如下代码的打印顺序:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t secondQueue = dispatch_queue_create("second.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t thirdQueue = dispatch_queue_create("third.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(firstQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(1.5);
NSLog(@" 运行在 firstQueue 中 %ld thread:%@",i,[NSThread currentThread]);
}
});
dispatch_async(secondQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.8);
NSLog(@" 运行在 secondQueue 中 %ld thread:%@",i,[NSThread currentThread]);
}
});
dispatch_async(thirdQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
NSLog(@" 运行在 thirdQueue 中 %ld thread:%@",i,[NSThread currentThread]);
}
});
}
希望不用贴出结果,打印顺序是无序的,当然,你可能找出某些顺序,但是,休眠时间是被设定好的,希望能够理解。但是当改动一下:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t secondQueue = dispatch_queue_create("second.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t thirdQueue = dispatch_queue_create("third.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t target = dispatch_queue_create("target.com", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(firstQueue, target);
dispatch_set_target_queue(secondQueue, target);
dispatch_set_target_queue(thirdQueue, target);
dispatch_async(firstQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
NSLog(@" 运行在 firstQueue 中 %ld thread:%@",i,[NSThread currentThread]);
}
});
dispatch_async(secondQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
NSLog(@" 运行在 secondQueue 中 %ld thread:%@",i,[NSThread currentThread]);
}
});
dispatch_async(thirdQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
NSLog(@" 运行在 thirdQueue 中 %ld thread:%@",i,[NSThread currentThread]);
}
});
}
2016-11-15 17:29:03.045 GS_GCD_Research[4461:1644317] 运行在 firstQueue 中 0 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.045 GS_GCD_Research[4461:1644317] 运行在 firstQueue 中 1 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.046 GS_GCD_Research[4461:1644317] 运行在 firstQueue 中 2 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.046 GS_GCD_Research[4461:1644317] 运行在 firstQueue 中 3 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.047 GS_GCD_Research[4461:1644317] 运行在 firstQueue 中 4 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.047 GS_GCD_Research[4461:1644317] 运行在 secondQueue 中 0 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.047 GS_GCD_Research[4461:1644317] 运行在 secondQueue 中 1 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.047 GS_GCD_Research[4461:1644317] 运行在 secondQueue 中 2 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.048 GS_GCD_Research[4461:1644317] 运行在 secondQueue 中 3 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.048 GS_GCD_Research[4461:1644317] 运行在 secondQueue 中 4 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.048 GS_GCD_Research[4461:1644317] 运行在 thirdQueue 中 0 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.048 GS_GCD_Research[4461:1644317] 运行在 thirdQueue 中 1 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.049 GS_GCD_Research[4461:1644317] 运行在 thirdQueue 中 2 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.049 GS_GCD_Research[4461:1644317] 运行在 thirdQueue 中 3 thread:{number = 4, name = (null)}
2016-11-15 17:29:03.049 GS_GCD_Research[4461:1644317] 运行在 thirdQueue 中 4 thread:{number = 4, name = (null)}
dispatch_set_target_queue能让多个串行队列在一个线程中依次执行。你可能会好奇,如果把目标队列target也设置成为并发呢?你可以试试哦。不过你想干嘛呢?你可能还会问,这三个队列若是并发的呢?还是顺序执行哦!
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t secondQueue = dispatch_queue_create("second.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t thirdQueue = dispatch_queue_create("third.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t target = dispatch_queue_create("target.com", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(firstQueue, target);
dispatch_set_target_queue(secondQueue, target);
dispatch_set_target_queue(thirdQueue, target);
你也可以看到,这么一来,线程的地址也是一样的,说明是同一条线程去处理了这些任务。线程居然是复用的,这样就能做到减少线程上下文切换造成的开销。
上面我们看到一个线程去处理多个串行队列的任务,尽管GCD不需要我们去显式地控制线程,可我发现一个有趣的事情。当一个队列被释放后,该队列原来的执行线程是否也被释放?别去无根据的猜测,为什么会有那么多人喜欢无根据地猜测呢?我们先看一段有意思的代码:
dispatch_queue_t serialQueue = dispatch_queue_create("gs.SERIAL.com", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("gs.SERIAL2.com", DISPATCH_QUEUE_SERIAL);
__block NSThread * thd = nil;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(serialQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
NSLog(@"在 serialQueue 中的处理线程地址:%p",[NSThread currentThread]);
}
thd = [NSThread currentThread];
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(serialQueue2, ^{
if ([[NSThread currentThread] isEqual:thd]) {
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
NSLog(@"在 serialQueue 中的处理线程地址:%p",[NSThread currentThread]);
}
}
});
打印结果:
2016-11-10 11:37:47.198 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.199 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.199 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.199 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.199 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.199 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.200 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.200 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.200 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
2016-11-10 11:37:47.200 GCDResearch[9070:3867429] 在 serialQueue 中的处理线程地址:0x78984ee0
你可以看到当serialQueue里的任务执行完毕后,空间地址为0x78984ee0的线程并没有释放掉,而是继续处理serialQueue2中的任务。GCD很聪明,他并不是为每一个队列都创建一个线程,能够线程复用减少系统开销的,GCD尽量去复用线程。设想你通过一个循环调用dispatch_async,企图开启上千条线程,你以为系统就这么让你肆意妄为吗?我就在iPhone6真机上企图调用dispatch_async 500次,查看GCD会创建多少条线程。请看如下代码:
__block NSMutableArray * marr = [NSMutableArray array];
for (NSInteger i = 0; i < 500; i++) {
dispatch_queue_t serialQueue = dispatch_queue_create("gs.SERIAL.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
sleep(0.5);
}
NSThread * thd = [NSThread currentThread];
//保证数组安全
dispatch_barrier_async(serialQueue, ^{
if (![marr containsObject:thd]) {
[marr addObject:thd];
}
NSLog(@"%ld",(unsigned long)marr.count);
});
});
}
在打印结果里,看到数组里的元素最多是274个,就是说GCD新开了274条线程,并不是开设了500条。GCD自身维持了一个新开线程的度,使你无法超过这个度。倘若你想通过其他如C的API去超过这个度,系统的资源无法平衡,你的软件就可以超度亡魂,往生极乐了。
3.3 任务的等待、分步提交、取消
3.3.1 等待
当你需要某个任务执行完成时在执行另外的任务,你可能使用如信号量或者锁等机制,在GCD中,当然也提供了这些,但是,还有一些API,使用来更加面向对象。如:
- (void)viewDidAppear:(BOOL)animated
{
dispatch_block_t task = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"进入任务,开始处理任务....休眠3秒......");
sleep(3.0);
NSLog(@"任务完成");
});
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, task);
// 等待task处理完成,等待期限是一辈子
dispatch_block_wait(task, DISPATCH_TIME_FOREVER);
NSLog(@"最后调用,task处理完成");
}
3.3.2 分步提交
有时,你并不想把任务一次性提交给队列,想分步提交,那么你只需要这么做:
- (void)viewDidAppear:(BOOL)animated
{
dispatch_block_t task = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"进入任务,开始处理任务....休眠3秒......");
sleep(3.0);
NSLog(@"任务完成");
});
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, task);
// 等待task任务处理完成后,将任务提交到queue中
dispatch_block_notify(task, queue, ^{
dispatch_assert_queue(queue);
NSLog(@"依旧在queue中处理任务");
});
}
3.3.3 取消
任务的取消,需要知道的是,取消的只能是未处理任务,正在处理的任务是无法取消的。
- (void)viewDidAppear:(BOOL)animated
{
dispatch_block_t task = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"进入任务,开始处理任务....休眠3秒......");
sleep(3.0);
NSLog(@"任务完成");
});
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, task);
//取消任务 这样就没有任何打印结果
dispatch_block_cancel(task);
//检查任务是否取消,若取消,返回一个非0值
if (dispatch_block_testcancel(task)) {
NSLog(@"取消成功");
}
}
3.4 队列的优先级Quality of Service(qos)
这一部分是我查看文档后也无法了解掌握的,或许这是一门失传的绝学,即使懂个一二,也无法完全把控代码。
3.5 Semaphores、Groups和Barriers在同步不同执行线程中的运用
看到Semaphores,你一下子可能就想到dispatch_semaphore_t。但不从GCD开始说,从传统并发编程中的信号量开始说起。
先说一个人,Edsger W. Dijkstra,1930生,2002年去世,荷兰人,计算机科学家,也是并发编程领域的先驱人物。是他提出了信号量机制。先记住两个荷兰语词汇吧,proberen:尝试,verhogen:增加。如果你记住了,这位科学家会高兴的,你可别梦到他,但也随你吧。
3.5.1 传统信号量机制
先说说线程被挂起,其实就是线程中断,该线程不会再被操作系统分配资源,也就是该线程不能分到CPU时间。在挂起状态,该线程里的所有代码都不会执行。
信号量(s)是一个全局的非负整数,对于这个变量,只能进行P和V操作,对,就是上面那两个单词。
- P:减法操作,当s != 0,P操作将s的值减去1,当s == 0,那么挂起当前线程,线程阻塞;
- V:加法操作,当s == 0,则V操作使s增加1,并且重启该挂起线程,当s != 0,则只是使s增加1。
一定要看清楚上面这两句话!信号量机制的原理就是这些。
在POSIX标准里,有如下API:
int sem_init(sem_t *sem, 0, unsigned int value);
int sem_wait(sem_t *s);
int sem_post(sem_t *s);
这三个API就是底层。sem_wait相当于P的减法操作,sem_post相当于V加法操作。
知道了这些,我们按GCD中的这三个东东。
3.5.2 dispatch_semaphore_t与AFN3.0的趣事
在著名的框架AFNetworking更新到3.0后,便不再支持网络请求同步。可我在github上看到有人一贴出这样的代码,问哪里出错,请看伪代码:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[manager GET:URLString
parameters:parameters
progress:^(NSProgress * _Nonnull downloadProgress) {}
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
dispatch_semaphore_signal(sema);
}
failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"为什么这里一定不走呢?");
如果你看懂了传统信号量机制中的两句话,这里不难解释吧?哦,还是难以解释?这....
说三点:
- AFN的网络请求是异步的,当请求完成,会返回到主线程;
- dispatch_semaphore_wait遇到信号量sema为0时,挂起当前线程,伪代码中,挂起了主线程;
- 当请求成功后,回到主线程调用dispatch_semaphore_signal企图对sema增加。哎,大哥,主线程被你挂起来了,你回到主线程干嘛呢!啊,这是干嘛呢!
你可能会问,怎么几道主线程被挂起了,恩,我喜欢这个问题。看如下代码:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^{
for (NSInteger i = 0; i < 100; i++) {
NSLog(@"循环中...");
sleep(1.0);
}
});
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
NSLog(@"吃个");
sleep(1.0);
});
NSLog(@"控制流检测");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"结束,完成");
}
//结果
2016-11-16 18:14:47.661 GS_GCD_Research[9153:2968704] 控制流检测
2016-11-16 18:14:47.661 GS_GCD_Research[9153:2968819] 吃个
循环不会执行,因为dispatch_semaphore_wait调用时,正好碰到信号量为0,主线程被挂起。
要不再给你演示一遍,看代码:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
NSLog(@"开始网络请求....");
sleep(1.0);
NSLog(@"请求成功,回调数据");
//相当于AFN请求完成后回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_semaphore_signal(sema);
});
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"永远不会被打印");
}
你企图在一个呗挂起的线程中发送一个信号,操作无效!我不能再解释了!
对了,指出了问题,总要给出解决办法吧!AFN请求完成后的回调是否只能是在主线程呢?人家考虑的很全,你可以指定回调到哪里。在AFN的源码里头有这么一段:
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
if (self.completionHandler) {
self.completionHandler(task.response, responseObject, serializationError);
}
.......
});
completionQueue是允许开发者指定的,只是默认为主队列。AFN的文档有这么写:
/**
The dispatch queue for `completionBlock`. If `NULL` (default), the main queue is used.
*/
@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue;
如果你看懂了,解决这个问题就不是难事。还不懂怎么解决?罢了,看下面代码:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString * const DownloadURL = @"http://www.test.com/test/test/image";
AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
dispatch_queue_t completionQueue = dispatch_queue_create("completion.com", DISPATCH_QUEUE_SERIAL);
manager.completionQueue = completionQueue;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[manager GET:DownloadURL parameters:nil progress:NULL success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
dispatch_semaphore_signal(sema);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"终于打印了");
}
3.5.3 dispatch_group任务组
其实,当你喜欢用上了GCD的信号量机制时,同步问题基本能解决,而接下来讲述的dispatch_group,也是为了解决同步问题。
dispatch_group的内部也有基于dispatch_semaphore,只是对dispatch_semaphore做的另外一层修饰。比如调用dispatch_group_enter时,内部其实是调用dispatch_semaphore_wait
void
dispatch_group_enter(dispatch_group_t dg)
{
dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;
(void)dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
}
说这么多,还没说group的出现到底是用做什么。当然是处理同步问题,包括下一节要说的barrier,都是为了处理同步问题。你可能会疑惑,GCD信号量不是解决了吗?怎么还要搞出这么多东东来。恩.....,很简单嘛,有些同步API很方便嘛,你不用写太多代码嘛,给你封装一层让你舒服嘛,还要其他理由来解释设计者的大爱吗!
group是为把任务关联到一个组上,有人把这个现象翻译成组队列。我.....,不欣赏,不赞同,不批评。个人觉得叫做任务组比较好,注意,我用了一个语气较温和的词“比较”。任务组可以提交多个任务,在任务组处理完所有任务时,有API去监听。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t secondQueue = dispatch_queue_create("second.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
// 当任务组中的任务全部处理完成,这个API允许开发者提交另外一个任务到指定的队列去处理,这个监听也不会造成阻塞;
// 且这个处理是异步的。
dispatch_group_notify(group, firstQueue, ^{
NSLog(@"任务组处理完成调用 %@",[NSThread currentThread]);
});
dispatch_group_async(group, firstQueue, ^{
NSLog(@"吃");
sleep(1.0);
});
dispatch_group_async(group, secondQueue, ^{
NSLog(@"吃");
sleep(0.5);
});
}
//结果
2016-11-17 15:16:30.251 GS_GCD_Research[11329:3728193] 吃
2016-11-17 15:16:30.251 GS_GCD_Research[11329:3728192] 吃
2016-11-17 15:16:31.290 GS_GCD_Research[11329:3728192] 任务组处理完成调用 {number = 3, name = (null)}
但是如下代码就不同:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t firstQueue = dispatch_queue_create("first.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t secondQueue = dispatch_queue_create("second.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, firstQueue, ^{
NSLog(@"吃");
sleep(1.0);
});
dispatch_group_async(group, secondQueue, ^{
NSLog(@"吃");
sleep(0.5);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"这句是同步等待所有任务处理完才执行,线程阻塞");
}
//结果
2016-11-17 15:27:01.967 GS_GCD_Research[11401:3756442] 吃
2016-11-17 15:27:01.967 GS_GCD_Research[11401:3756459] 吃
2016-11-17 15:27:03.042 GS_GCD_Research[11401:3756311] 这句是同步等待所有任务处理完才执行,线程阻塞
dispatch_group_wait其实是挂起了当前线程。
3.5.3 dispatch_group_async的替代
在关于group的GCD中,有两个诡异的API,dispatch_group_enter和dispatch_group_leave。如何理解这两个方法呢?先看看用法:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_group_t group = dispatch_group_create();
// 代表将吃的任务加入到任务组group
dispatch_group_enter(group);
dispatch_async(firstQueue, ^{
sleep(1.0);
NSLog(@"吃");
// 代表把将吃的任务从任务组中扔出
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(secondQueue, ^{
sleep(0.5);
NSLog(@"吃");
dispatch_group_leave(group);
});
dispatch_queue_t gQueue = dispatch_queue_create("group.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_notify(group, gQueue, ^{
NSLog(@"任务组处理完成调用 %@",[NSThread currentThread]);
});
}
//结果
2016-11-17 17:15:55.825 GS_GCD_Research[11942:3950300] 吃
2016-11-17 17:15:56.888 GS_GCD_Research[11942:3950301] 吃
2016-11-17 17:15:56.888 GS_GCD_Research[11942:3950301] 任务组处理完成调用 {number = 3, name = (null)}
这样就保证了任务全部结束后,再处理dispatch_group_notify。
3.5.3 dispatch_barrier
barrier是个好东西!性价比很高,几句代码解决同步问题:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"吃个");
});
dispatch_async(queue, ^{
NSLog(@"吃个");
});
// 这段代码能够保证前面两个任务执行完毕再执行。当然,你可以用group、信号量去实现,但是,这个多简洁!!!
dispatch_barrier_async(queue, ^{
NSLog(@"设置障碍");
});
dispatch_async(queue, ^{
NSLog(@"吃个");
});
}
//结果
2016-11-17 17:34:31.459 GS_GCD_Research[12138:4028187] 吃个
2016-11-17 17:34:31.459 GS_GCD_Research[12138:4028203] 吃个
2016-11-17 17:34:31.459 GS_GCD_Research[12138:4028203] 设置障碍
2016-11-17 17:34:31.459 GS_GCD_Research[12138:4028203] 最后吃个
不过barrier有个注意点,在文档中:
Submits a block to a dispatch queue like dispatch_async(), but marks that block as a barrier (relevant only on DISPATCH_QUEUE_CONCURRENT queues).
其实,在琢磨这句话时,觉得是不是GCD的文档编写者在开玩笑。如果是串行队列,那么好设置什么障碍啊,就是依次执行,不信你看:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t queue = dispatch_queue_create("queue.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"吃个");
sleep(0.8);
});
dispatch_async(queue, ^{
NSLog(@"吃个");
sleep(0.8);
});
dispatch_async(queue, ^{
NSLog(@"最后吃个");
});
}
//结果
2016-11-17 17:41:19.923 GS_GCD_Research[12209:4055728] 吃个
2016-11-17 17:41:19.924 GS_GCD_Research[12209:4055728] 吃个
2016-11-17 17:41:19.924 GS_GCD_Research[12209:4055728] 最后吃个
四篇文章参考资料:
1.王越:Mac OS X背后的故事(原链接已失效,还请自行搜索,实在是有趣的文章)
2.底层并发 API
3.Concurrency Programming Guide
4.Computer Systems: A Programmer's Perspective
本作品采用采用 知识共享署名-非商业性使用-禁止演绎 3.0 中国大陆许可协议进行许可