SerendipityEx

关注成长,记录生活

读:《Objective-C高级编程》之GCD

什么是GCD

Grand Central Dispatch(GCD)是异步执任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发只需要定义想执行并追加到适当的Dispatch Queue 中,GCD就能生成必要的线程并计划执行任务,由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可以执行任务,这样就比以前的线程更有效率。

关于多线程编程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流。 一个CPU 一次只能执行一个命令,不能执行某处分开的并列的两个命令。因此通过CPU执行的CPU命令就好比一条无分叉的大道,其执行不会出现分歧。

OS XiOS的核心XNU内核在发生操作系统事件时,会切换执行路径,执行中的状态,例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令行。这就称为上下文切换

由于使用多线程的程序可以在某个线程和其他线程之间反复多次的进行上下文切换,因此看上去就好像1个CPU核能够并列的执行多个线程一样,而且在具有多个CPU核的情况下,就是真的提供多个CPU核并行执行多个线程的技术。

这种利用多线程编程的技术就称为“多线程编程”。

但是,多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互持续等待(死锁)、使用太多线程会消耗大量内存等。

GCD 的 API

Dispatch Queue

dispatch_async(dispatch_queue_t  _Nonnull queue, ^{
   // doSomething     
});

Dispatch Queue, 是执行处理的等待队列。

使用Block语法定义想执行的任务,通过dispatch_async函数追加赋值在变量queue的Dispatch Queue中。仅这样就可以使指定的Block在另外一个线程执行。

Dispatch Queue按照FIFO(First-In-First-Out)执行处理。

Dispatch Queue有两种:

Dispatch Queue 说明
Serial Dispatch Queue 等待现在执行中处理结果
Concurrent Dispatch Queue 不等待现在执行中处理结果

dispatch_queue_create

通过dispatch_queue_create生成Dispatch Queue。

如下生成一个 Serial Dispatch Queue:

dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr);

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);

第一个参数指定Dispatch Queue的名称,第二个参数指定NULL/DISPATCH_QUEUE_SERIAL 生成Serial Dispatch Queue。。生成Concurrent Dispatch Queue时,将第二个参数指定为DISPATCH_QUEUE_CONCURRENT,如下:

dispatch_queue_t myConcurretnDispatchQueue = dispatch_queue_create("com.example.gcd.myConCurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create 函数的返回值为表示Dispatch Queuede的 dispatch_queue_t类型。

Serial Dispatch Queue 一次只能执行一个追加任务。系统对一个Serial Dispatch Queue只生成一并使用一个线程,如果生成2000个Serial Dispatch Queue,那么就生成2000个线程。如果过多使用多线程就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。

只在为了避免多线程编程问题之一——多个线程更新相同资源导致数据竞争时,使用Serial Dispatch Queue

下面分别添加几个block到两个队列中: Serial Dispatch Queue

	dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);
    dispatch_async(mySerialDispatchQueue, ^{ NSLog(@"blcok1"); });
    dispatch_async(mySerialDispatchQueue, ^{ NSLog(@"block2"); });
    dispatch_async(mySerialDispatchQueue, ^{ NSLog(@"block3"); });
    dispatch_async(mySerialDispatchQueue, ^{ NSLog(@"block4"); });
    ...

输出:

2017-01-19 20:23:50.599 gcd[13765:2856357] blcok1
2017-01-19 20:23:50.599 gcd[13765:2856357] block2
2017-01-19 20:23:50.599 gcd[13765:2856357] block3
2017-01-19 20:23:50.599 gcd[13765:2856357] block4

Concurrent Dispatch Queue

    dispatch_queue_t myConcurretnDispatchQueue = dispatch_queue_create("com.example.gcd.myConcurretnDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurretnDispatchQueue, ^{ NSLog(@"blcok1"); });
    dispatch_async(myConcurretnDispatchQueue, ^{ NSLog(@"block2"); });
    dispatch_async(myConcurretnDispatchQueue, ^{ NSLog(@"block3"); });
    dispatch_async(myConcurretnDispatchQueue, ^{ NSLog(@"block4"); });
    ...

输出:

2017-01-19 20:25:17.937 gcd[13811:2867322] blcok1
2017-01-19 20:25:17.937 gcd[13811:2867323] block3
2017-01-19 20:25:17.937 gcd[13811:2867325] block2
2017-01-19 20:25:17.937 gcd[13811:2867366] block4
...

在 iOS 6+ 和 Mac OS X 10.8+ ARC 可以管理GCD.在此之前,用下面代码释放gcd

dispatch_release(queue);

Main Dispatch Queue/Global Dispatch Queue

Main Dispatch Queue 是在主线程中执行的Dispatch Queue。 是Serial Dispatch Queue。 追加到 Main Dispatch Queue 的处理实在主线程的RunLoop 中执行,由于是在主线程中执行,因此要将用户界面更新等一些必须在主线程中执行的处理追加Main Dispatch Queue。 Global Dispatch Queue 是所有应用程序能够使用的 Concurrent Dispatch Queue。没必要通过dispatch_queue_create 逐个生成Concurrent Dispatch Queue。只要的Global Dispatch Queue中使用即可。

//	Main Dispatch Queue
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    /*
     * Global Dispatch Queue 优先级从高到低
	 */

    dispatch_queue_t globalQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

    dispatch_queue_t globalQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_queue_t globalQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

    dispatch_queue_t globalQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

下面是最常见的Main Dispatch Queue和Global Dispatch Queue的使用方法。

	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       /*
        *   可并行的处理
        */
        dispatch_async(dispatch_get_main_queue(), ^{
            //只能在主线程中执行的处理
        });
    });

dispatch_set_target_queue

dispatch_set_target_queue函数是变更生成的Dispatch Queue的优先级。

修改用户队列的目标队列,使多个serial queue在目标queue上一次只有一个执行。

 // dispatch_set_target_queue(<#dispatch_object_t  _Nonnull object#>, dispatch_queue_t  _Nullable queue)

   dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

   dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

指定要变更执行优先级的Dispatch Queue 为 dispatch_set_target_queue函数的第一个参数,指定与要使用的执行优先级相同优先级的Global Dispatch Queue为第二个参数(目标)。第一个参数如果指定系统提供的Main Dispatch Queue 和 Global Dispatch Queue 则不知道会出现什么情况。因为你不能给系统的queue设置权限,所以这些均不可指定。

一般都是把一个任务放到一个串行的queue中,如果这个任务被拆分了,被放置到多个串行的queue中,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。这时使用dispatch_set_target_queue将多个串行的queue指定到了同一目标,那么着多个串行queue在目标queue上就是同步执行的,不再是并行执行。

dispatch_after

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3 * NSEC_PER_SEC));
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(33 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"waited at least three seconds");
    });

dispatch_after 函数并不是在指定时间后执行,只是在指定时间追加处理到Dispatch Queue中。

第一个参数是指定时间用的dispatch_time_t 类型的值。使用dispatch_time 或者dispatch_walltimeb

第二个参数指定要追加处理的Dispatch Queuede,第三个参数指定执行处理的block。

Dispatch Group

在追加到Dispatch Queue中的多个处理全部结束后执行结束处理。

  • 将执行的处理全部追加到 一个Serial Dispatch Queue中,并在最后追加结束处理。
  • 但是在并行队列中可以使用Dispatch Group实现。

Target:追加3个blockGlobal Dispatch Queue 中,这些Block如果全部执行完成,就会执行Main Dispatch Queue中结束处理的Block

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_async(group, queue, ^{  NSLog(@"blk0"); });
    dispatch_group_async(group, queue, ^{  NSLog(@"blk1"); });
    dispatch_group_async(group, queue, ^{  NSLog(@"blk2"); });
    dispatch_group_async(group, queue, ^{  sleep(3); NSLog(@"blk3"); });
    dispatch_group_async(group, queue, ^{  NSLog(@"blk4"); });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"over");
    });

解析: 向并行线程追加处理,多个线程并行执行,所以执行的顺序不定。 但是”over”一定是最后执行的。另外,也可以使用dispatch_group_wait函数,仅等待全部处理执行结束。

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_async(group, queue, ^{  NSLog(@"blk0"); });
    dispatch_group_async(group, queue, ^{  NSLog(@"blk1"); });
    dispatch_group_async(group, queue, ^{  NSLog(@"blk2"); });
    dispatch_group_async(group, queue, ^{  sleep(3); NSLog(@"blk3"); });
    dispatch_group_async(group, queue, ^{  NSLog(@"blk4"); });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"over");

dispatch_group_wait的二个参数是等待的时间。如果返回值为0,则表示所有的任务执行完毕。否则,group中仍有任务在执行。

dispatch_barrier_async

在访问数据库或文件时,使用Serial Dispatch Queue能避免数据竞争的问题。

写入处理确实不能与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。

Dispatch_barrier_async函数的处理流程

首先dispatch_queue生成函数Concurrent Dispatch Queue,在dispatch_async中追加读取操作。

dispatch_queue_t = queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);

dispatch_sync

dispatch_async函数是非同步的,即将指定的Block非同步的加入到指定的Dispatch Queue中,dispatch_sync函数不做任何等待。

dispatch_sync函数同步的将Block追加到Dispatch Queue中,在Block结束之前,函数会一直等待。

经常使用的一种情形是:执行Main Dispatch Queue时,使用另外的线程Global Dispatch Queue进行处理,处理结束后立即使用所得到的结果。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
  /*
   处理
   */
   dispatch_sync(dispatch_get_main_queue(), ^{
     // 主线程操作,如刷新UI
     });
  });

一旦调用dispatch_sync函数,那么在指定的处理结束之前,该函数都不会返回。也是因为这个特性,在使用不当时,便会造成一个严重的问题,即死锁

例如在主线程中使用:

dispatch_sync(dispatch_get_main_queue(), ^{
  NSLog(@"Hello????");
  });

dispatch_apply

dispatch_apply函数是dispatch_syncDispatch Group的关联API。 它按指定的次数将指定的Block追加到指定的Dispatch Queue,并等待全部处理执行结束。

dispatch_queue queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index){
  NSLog(@"%zu",index);
  });
  NSLog(@"Done"); // 最后执行!

dispatch_suspend/dispatch_resume

dispatch_suspend挂起指定的Dispatch Queue。 ‵‵‵ dispatch_suspend(queue); ‵‵‵ dispatch_resume恢复指定的Dispatch Queue

dispatch_once

static int initialized = NO;
if (initialized == NO) {
    /*
     * 初始化
     */
}
static dispatch_once_t pred;
dispatch_once(&pred, ^{
   /*
    * 初始化
    */
});

通过dispatch_once 函数,代码即使在多线程环境下执行,也可保证百分之百安全。

第一组代码在大多数情况下也是安全的。但在多核CPU中,在正在更新的表示是否初始化的标志变量时,读取,就有可能多次执行初始化处理。而用dispatch_once函数初始化就不必担心这样的问题。

GCD外传:dispatch_once(上)

Swift:Automatic Reference Counting in Swift