iOS多线程-GCD的基础概念和API

GCD是苹果推出的一套从底层线程编程中抽象出的一种基于队列来管理任务的方式。相对于直接在线程上执行任务,使用GCD的方式更加简便和高效。

通过GCD,调用者可以同步或者异步将要执行的任务放进相应的队列中等待执行,队列采用先进先出的方式取出里面的任务,然后将任务分配到相应的线程中执行。

理解GCD需要理解几个关键的概念和问题:

dispatch_async和dispatch_sync(异步执行和同步执行)

同步执行和异步执行是相对于当前调用的线程而言的。也就是说,dispatch_sync会阻塞当前的线程而dispatch_async不会。

并发队列和串行队列

串行队列中的任务是一个个按顺序完成的。后一个任务必须等待前一个任务完成。而并发队列中的任务是可以同时进行的。同时进行意味者在并发队列中同时进行的任务一定是在不同的线程中执行的。

队列和线程的关系

GCD提供了一个串行的主队列和若干个全局并发队列。主队列中的所有任务都会在主线程中执行,而全局并发队列会根据应用当前执行的情况来给任务分配线程,包括主线程

重要:虽然下面会大量地提到线程,但是GCD的核心在于如何通过队列的方式来思考和执行相应的任务来达到想要的效果,大部分时候都可以忽略线程,但是在学习的时候我们可以多考虑一下,加深对GCD的理解

自己创建的队列中的任务最终会进入到默认优先级的全局队列或者主队列。关系可见官方的配图:
iOS多线程-GCD的基础概念和API_第1张图片
gcd_queue_thread

可以通过一些简单的代码来验证一下上面的内容:

    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.async", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"async block 1: %@",[NSThread currentThread]);
    });
    
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"async block 2: %@",[NSThread currentThread]);
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);

运行结果如下:

2017-04-18 17:34:56.905 ConcurrentProgram[89385:39591640] Before excute: {number = 1, name = main}
2017-04-18 17:34:56.906 ConcurrentProgram[89385:39591640] After excute: {number = 1, name = main}
2017-04-18 17:34:58.911 ConcurrentProgram[89385:39593236] async block 2: {number = 3, name = (null)}
2017-04-18 17:34:59.910 ConcurrentProgram[89385:39593020] async block 1: {number = 4, name = (null)}

分析:Before excute和After excute都是在主线程中完成的。在两者间主线程调用了两次dispatch_async方法来将两个任务,block1和block2放入自定义的并发队列中。从运行结果看,先完后After Excute,说明主线程的执行并没有因为调用dispatch_async而阻塞。从时间上看,block2先于block1完成,并且二者相差一秒,刚好是两者sleep的时间的差。说明这两个任务是同时执行的,这一点还可以在打印出来的线程地址不一致中得到印证。

过程如下:

  1. 主线程打印Before excute
  2. 主线程将block1放入自定义并发队列中异步执行,并发队列开线程A执行block1,
  3. 主线程继续执行,将block2放入并发队列中异步执行,并发队列取出block2,再开辟新的线程B执行block2
  4. 主线程继续执行,打印After excute
  5. B线程在休眠两秒后打印
  6. A线程在休眠3秒后打印

再看下一个示例:

- (void)dispatchSyncFromMainToConcurrentQueue{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.sync", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_sync(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"async block 1: %@",[NSThread currentThread]);
    });
    
    dispatch_sync(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"sync block 2: %@",[NSThread currentThread]);
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);
}

结果如下:

2017-04-19 10:31:51.647 ConcurrentProgram[29439:40557792] Before excute: {number = 1, name = main}
2017-04-19 10:31:54.648 ConcurrentProgram[29439:40557792] async block 1: {number = 1, name = main}
2017-04-19 10:31:56.649 ConcurrentProgram[29439:40557792] sync block 2: {number = 1, name = main}
2017-04-19 10:31:56.649 ConcurrentProgram[29439:40557792] After excute: {number = 1, name = main}

过程如下:

  1. 主线程打印before excute
  2. 主线程将block1放入并发队列中同步执行,此时主线程阻塞
  3. 并发队列取出block1,将block1分配给主线程执行,block1打印
  4. block1执行完毕后,主线程继续执行,将block2放入队列中,
  5. 队列将block2分配给主线程执行,block2打印
  6. block2执行完毕后主线程继续执行,打印after excute

结论:

  1. 放进同一个并发队列中的不同任务不一定就会分配到不同的线程
  2. 并发队列也可能将队列中的任务交由主线程执行

推测:在调用dispatch_sync时,由于调用的线程阻塞,或者在等待dispatch的任务完成之前是空闲的,那么dispatch的队列很可能将该任务交由调用的线程执行,这也是有些文章在说dispatch_sync和dispatch_async的区别是会说一个不会新开线程一个会新开线程,其实这个说法不正确。如果dispatch_sync或者dispatch_async的队列参数是主队列,那么一定会放进主线程中执行,此时不存在开不开线程的问题。dispatch_sync和dispatch_async的根本区别还是在于是否会阻塞调用该方法的线程。

再看一下串行队列的例子

- (void)serialQueue{
    dispatch_queue_t serialQueue = dispatch_queue_create("serial", nil);
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(serialQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"async block 1: %@",[NSThread currentThread]);
    });
    
    dispatch_sync(serialQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"sync block 2: %@",[NSThread currentThread]);
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);
}
2017-04-19 10:58:56.296 ConcurrentProgram[30778:40656851] Before excute: {number = 1, name = main}
2017-04-19 10:58:59.297 ConcurrentProgram[30778:40657162] async block 1: {number = 3, name = (null)}
2017-04-19 10:59:01.298 ConcurrentProgram[30778:40656851] sync block 2: {number = 1, name = main}
2017-04-19 10:59:01.299 ConcurrentProgram[30778:40656851] After excute: {number = 1, name = main}

过程:

  1. 主线程打印before
  2. 主线程将block1放入串行队列中异步执行,串行队列取出任务,开辟线程A执行block1(由于主线程此时是不阻塞的,还要继续执行,因此必须新开一个线程执行block1)
  3. 此时主线程继续执行,将block2放入串行队列中同步执行,主线程阻塞,等待block2执行完毕。
  4. 由于是串行队列,block2必须等待block1执行完毕。
  5. 线程A执行block1完毕,串行队列取出block2并分配给主线程执行并打印(因为block2执行完之前主线程必须等待,让主线程执行完全没有问题,省下开辟线程的开销)
  6. 最后主线程打印aftert

一个常见的用法:

    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //do some calcutate
        [NSThread sleepForTimeInterval:3];
        NSLog(@"block calculate: %@",[NSThread currentThread]);
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"main block upadte: %@",[NSThread currentThread]);
        });
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);

在主线程的执行过程中异步地将一个需要耗时的任务放入全局并发队列,此时队列会使用非主线程执行该任务,等待任务完成后在将更新UI的任务放入主队列,主队列分配给主线程执行更新UI的任务。

一个不要犯的错误:

    dispatch_queue_t queue = dispatch_queue_create("wrong", nil);
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(queue, ^{
        NSLog(@"block 1: %@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"block2 %@",[NSThread currentThread]);
        });
        NSLog(@"after block2");
    });
    
    NSLog(@"After excute: %@",[NSThread currentThread]);

会造成EXEC_BAD_INSTRUCTION的错误。在官方文档中有这样一句提示:

Important: You should never call the dispatch_sync or dispatch_sync_f function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.

永远不要在一个队列中调用dispatch_sync方法到同一个队列中,尤其是在串行队列(一定死锁),在并发队列中也应该避免使用。

来分析一下为什么:

串行队列正在执行任务A,中间调用了dispatch_async要分派一个新的任务B(block)给队列自己,此时正在执行的线程被阻塞等待任务B执行,但是任务B在串行队列中排在任务A后面需要等待A完成后执行。于是B等待A,A等待B。死锁。

并发队列虽然没有上述死锁问题(因为并发队列不需要等待上一个任务完成)。要避免这样写估计是习惯问题。因为大多数情况下dispatch到其他的队列效果是一样的而不会造成误解。

dispatch_barrier_async

dispatch_barrier_async的意义在于,对于并发队列而言,在某些时候可以像串行队列一样控制任务的执行顺序。

假设有这样的需求,任务A执行30s后得到结果A1,任务B需要执行20s后得到结果B1,任务C的运算依赖于A1 和 B1,得到结果C1,任务D的处依赖于C1。那么我们要保证任务C在任务A和B执行完之后,D在任务C执行完以后才能有正确的结果。当然我们可以将ABCD放进一个串行队列,但是如果A和B都是非常耗时的任务,那么会造成计算时间上的浪费。因为A和B是可以并发计算的。这个时候采用dispatch_barrier_async就非常有意义了。

    dispatch_queue_t queue = dispatch_queue_create("My concurrent queue", DISPATCH_QUEUE_CONCURRENT);
    __block int a1 = 0;
    __block int b1 = 0;
    __block int c1 = 0;
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:3];
        a1 = 5;
        NSLog(@"A finish");
    });
    
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        b1 = 10;
        NSLog(@"B finish");
    });
    
    dispatch_barrier_async(queue, ^{
        c1 = a1+b1;
        NSLog(@"handle A1 and B1");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"handle C1");
    });

运行结果为:

2017-04-19 15:18:32.106 ConcurrentProgram[41116:41124691] B finish
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41124651] A finish
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41123629] handle A1 and B1
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41124651] handle C1

dispatch_barrier_async中的任务会等待其队列中在它之前的任务完成后执行,并且在该任务后面加入队列的任务(如最后一个block)会等待该任务执行完之后才会开始执行。

这个API文档中的discussion里有这样一句话:

The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.

也就是说这个API只有对于自己创建的并发队列是有用的,否则就和dispatch_async效果是一样的。

我们考虑一下如果是串行队列,那么本来里面的任务就是必须按顺序一个接一个完成,所以使用这个API是没有意义的。问题在于为什么系统提供的全局并发队列无法达到相应的效果呢?

个人猜测是这样的:上面说过,自己创建的队列中的任务最终会放入到系统的全局队列中或者主队列中,那么这个API可能只是控制了在什么时机将队列中的任务放进全局队列中去。采用刚刚的例子,就是自己创建的queue中有ABCD四个任务,queue将A和B先放入全局队列,等待A和B都执行完以后,将C放进全局队列,C执行完成后再将D放进全局队列中。所以全局队列中本身是没有这种机制的。

参考资料:

https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW2

https://objccn.io/issue-2-2/

http://www.jianshu.com/p/06a18323d9d2

http://www.humancode.us/2014/08/06/concurrent-queues.html

你可能感兴趣的:(iOS多线程-GCD的基础概念和API)