【iOS】GCD

基础知识

什么是GCD?以下摘自苹果的官方说明。

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

任务和队列

学习 GCD 之前,先来了解 GCD 中两个核心概念:任务队列

任务

就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行异步执行

  • 两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力
同步执行(sync)
  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async)
  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力。

队列(Dispatch Queue)

队列(Dispatch Queue)里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出) 的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

  • 队列的结构可参考下图:
    在这里插入图片描述

在 GCD 中有两种队列:串行队列并发队列。两者都符合 FIFO(先进先出) 的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

串行队列(Serial Dispatch Queue)
  • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue)
  • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

两者具体区别如下两图所示:
在这里插入图片描述

在这里插入图片描述

同步异步、串行并行和线程的关系

在这里插入图片描述

由上图我们可以得出我们有3种队列,2种任务执行方式,那么我们就有了6种不同的组合方式,分别是:

  1. 并行队列 + 同步执行
  2. 并行队列 + 异步执行
  3. 串行队列 + 同步执行
  4. 串行队列 + 异步执行
  5. 主队列 + 同步执行
  6. 主队列 + 异步执行

那么这几种不同组合方式各有什么区别:

队列并发队列串行队列主队列
同步 (sync)没有开启新线程,串行执行任务没有开启新线程,串行执行任务主线程调用:死锁卡住不执行其他线程调用:没有开启新线程,串行执行任务
异步 (async)有开启新线程,并发执行任务有开启新线程(1条),串行执行任务没有开启新线程,串行执行任务

需要注意的几个地方:

  • 异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。
  • 并发队列的并发功能只有在异步(dispatch_async)函数下才有效。
  • 主队列是一种特殊的串行队列。

GCD

首先回顾一下苹果官方对GCD的说明。

  • 开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。

这句话用源代码表示如下:

dispatch_async(queue, ^ {
	/*
	 * 想执行的任务
	 */

});

该源代码使用Block语法“定义想执行的任务”,通过dispatch_async函数“追加”赋值在变量queue的“Dispatch Queue中”。今这样就可使指定的Block在另一线程中执行。

“Dispatch Queue” 是什么呢?

  • 如其名称所示,是执行处理的等待队列。应用程序编程人员通过dispatch_async函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序 (先进先出FIFO,First-In-First-Out) 执行处理。

请添加图片描述

另外在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue。如下所示。

请添加图片描述

请添加图片描述

比较这两种Dispatch Queue。准备以下源代码,在dispatch_async中追加多个处理。

dispatch_async(queue, blk0);
dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
dispatch_async(queue, blk5);
dispatch_async(queue, blk6);
dispatch_async(queue, blk7);

当变量queueSerial Dispatch Queue时,因为要等待现在执行中的处理结束,所以首先执行blk0blk0执行结束后,接着执行blk1blk1结束后再开始执行blk2,如此重复。同时执行的处理数只能有1个。即执行该源代码后,一定按照以下顺序进行处理。

blk0
blk1
blk2
blk3
blk4
blk5
blk6
blk7

当变量queueConcurrent Dispatch Queue 时,因为不用等待现在执行中的处理结束,所以首先执行blk0,不管blk0的执行是否结束,都开始执行后面的blk1,不管blk1的执行是否结束,都开始执行后面的blk2,如此重复循环。

这样虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态。即 iOS和OS X基于Dispatch Queue中的处理数、处理CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并行执行的处理数。所谓“并行执行”,就是使用多个线程同时执行多个处理。如下图所示。

请添加图片描述

GCD的使用步骤

队列的创建

可以使用dispatch_queue_create来创建对象,需要传入两个参数:

  • 第一个参数表示队列的唯一标识符,用于DEBUG,可为空;
  • 第二个参数用来识别是串行队列还是并行队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并行队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_SERIAL);
// 并行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_CONCURRENT);
// 全局并行队列。GCD默认提供了全局的并行队列,需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    });

队列的获取

  • 队列的获取(这里说的是两个特殊的队列,主队列和全局并发队列):
//主队列:
dispatch_queue_t queue = dispatch_get_main_queue();

//并发队列:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

任务的创建

// 同步执行任务创建方法
dispatch_sync(queue, ^{
 // 执行的任务
    NSLog(@"%@",[NSThread currentThread]);   
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
 // 执行的任务
    NSLog(@"%@",[NSThread currentThread]);    
});

需要注意的几个地方:

  1. 所有放在主队列中的任务,都会放到主线程中执行。
  2. GCD 默认提供了全局并发队列(Global Dispatch Queue)。

GCD的基本使用

同步执行 并发队列

  • 在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
/**
 * 同步执行 并发队列
 * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncConcurrent {
            NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
            NSLog(@"syncConcurrent---begin");
            dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
            dispatch_sync(queue, ^{
                // 追加任务1
                for (int i = 0; i < 2; i++) {
                    [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                    NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
                }
            });
            dispatch_sync(queue, ^{
                // 追加任务2
                for (int i = 0; i < 2; i++) {
                    [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                    NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
                }
            });
            dispatch_sync(queue, ^{
                // 追加任务3
                for (int i = 0; i < 2; i++) {
                    [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                    NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
                }
            });
            NSLog(@"syncConcurrent---end");
}

运行结果:

在这里插入图片描述

从同步执行 并发队列中可看到:

  • 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
  • 所有任务都在打印的syncConcurrent---beginsyncConcurrent---end之间执行的(同步任务需要等待队列的任务执行结束)。
  • 任务按顺序执行的。按顺序执行的原因:虽然并发队列可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。

异步执行 并发队列

  • 可以开启多个线程,任务交替(同时)执行。
/**
 * 异步执行 并发队列
 * 特点:可以开启多个线程,任务交替(同时)执行。
 */
- (void)asyncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncConcurrent---begin");
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    NSLog(@"asyncConcurrent---end");
}

运行结果:

请添加图片描述

在异步执行 并发队列中可以看出:

  • 除了当前线程(主线程),系统又开启了3个线程,并且任务是交替/同时执行的。(异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务)。
  • 所有任务是在打印的syncConcurrent---beginsyncConcurrent---end之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行不做等待,可以继续执行任务)。

同步执行 串行队列

不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。

/**
 * 同步执行 串行队列
 * 特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
 */
- (void)syncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncSerial---begin");
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    NSLog(@"syncSerial---end");
}

运行结果:

请添加图片描述

在同步执行 串行队列可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行不具备开启新线程的能力)。
  • 所有任务都在打印的syncConcurrent---beginsyncConcurrent---end之间执行(同步任务需要等待队列的任务执行结束)。
  • 任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。

异步执行 串行队列

  • 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
- (void)asyncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncSerial---begin");
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    NSLog(@"asyncSerial---end");
}

运行结果:

请添加图片描述

在异步执行 串行队列可以看到:

  • 开启了一条新线程(异步执行具备开启新线程的能力,串行队列只开启一个线程)。
  • 所有任务是在打印的syncConcurrent---beginsyncConcurrent---end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。

同步执行 主队列

同步执行 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。

在主线程中调用同步执行 主队列
  • 互相等待卡住不可行
/**
 * 同步执行 主队列
 * 特点(主线程调用):互等卡主不执行。
 * 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMain---begin");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    NSLog(@"syncMain---end");
}

运行结果:
请添加图片描述

在同步执行 主队列可以惊奇的发现:

在主线程中使用同步执行 主队列,追加到主线程的任务1、任务2、任务3都不再执行了,而且syncMain---end也没有打印,在XCode上还会报崩溃。这是为什么呢?

  • 这是因为我们在主线程中执行syncMain方法,相当于把syncMain任务放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把任务1追加到主队列中,任务1就在等待主线程处理完syncMain任务。而syncMain任务需要等待任务1执行完毕,才能接着执行。
  • 那么,现在的情况就是syncMain任务和任务1都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,而且syncMain---end也没有打印。
在其他线程中调用同步执行 主队列
  • 不会开启新线程,执行完一个任务,再执行下一个任务
- (void)viewDidLoad {
    [super viewDidLoad];
    // 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行selector 任务
    [NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
}

- (void)syncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMain---begin");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_sync(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    NSLog(@"syncMain---end");
}

运行结果:

请添加图片描述

在其他线程中使用同步执行 主队列可看到:

  • 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
  • 所有任务都在打印的syncConcurrent---beginsyncConcurrent---end之间执行(同步任务需要等待队列的任务执行结束)。
  • 任务是按顺序执行的(主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

为什么现在就不会卡住了呢?

  • 因为使用 NSThreaddetachNewThreadSelector 方法会创建线程,并自动启动线程执行selector 任务,syncMain 任务放到了其他线程里,而任务1、任务2、任务3都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2任务3。所以这里不会卡住线程。

异步执行 主队列

  • 只在主线程中执行任务,执行完一个任务,再执行下一个任务。
/**
 * 异步执行 主队列
 * 特点:只在主线程中执行任务,执行完一个任务,再执行下一个任务
 */
- (void)asyncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncMain---begin");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    NSLog(@"asyncMain---end");
}

运行结果:

请添加图片描述

在异步执行 主队列可以看到:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
  • 所有任务是在打印的syncConcurrent---beginsyncConcurrent---end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(因为主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

dispatch_set_target_queue

dispatch_queue_create函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue函数。在后台执行动作处理的Serial Dispatch Queue的生成方法如下:

请添加图片描述

指定要变更执行优先级的Dispatch Queuedispatch_set_target_queue函数的第一个参数,指定与要使用的执行优先级的Global Dispatch Queue为第二个参数(目标)。第一个参数如果指定系统提供的Main Dispatch QueueGlobal Dispatch Queue则不知道会出现什么状况,因此这些均不可指定。

Dispatch Queue指定为dispatch_set_target_queue函数的参数,不仅可以变更Dispatch Queue的执行优先级,还可以作成Dispatch Queue的执行阶层。如果在多个Serial Dispatch Queue中用dispatch_set_target_queue函数指定目标为某一个Serial Dispatch Queue,那么原先本应并行执行的Serial Dispatch Queue,在目标Serial Dispatch Queue,那么原本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能同时执行一个处理。

请添加图片描述

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

dispatch_after

经常会有这样的情况:想再3秒后执行处理。可能不仅限于3秒,总之,这种想在指定时间后执行处理的情况,可食用dispatch_after函数来实现。

在3秒后将指定的Block追加到Main Dispatch Queue中的源代码如下:

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

需要注意的是,dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue。此源代码与在3秒后用dispatch_async函数追加BlockMain Dispatch Queue的相同。

因为Main Dispatch Queue在主线程的Runloop中执行,所以再比如每隔1/60秒执行的Runloop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或主线程的处理本身有延迟时,这个时间会更长。

虽然在有严格时间的要求下使用时会出现问题,但在想大致延迟执行处理时,该函数是非常有效的。

  • 另外,第二个参数指定要追加处理的Dispatch Queue,第三个参数指定记述要执行处理的Block。
  • 第一个参数是指定时间用的dispatch_time_t类型的值。该值使用dispatch_time函数或dispatch_walltime函数作成。

dispatch_time函数能够获取从第一个参数dispatch_time_t类型之中指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。第一个参数经常使用的值是之前源代码中出现的DISPATCH_TIME_NOW。这表示现在的时间。即以下源代码可得到表示从现在开始1秒后的dispatch_time_t类型的值。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);

数值和NSEC_PER_SEC的乘积的到单位为毫微秒的数值。“ull”是c语言的数值字面量,是显式表明类型时使用的字符串(表示“unsigned long long”)。如果使用NSEC_PER_MSEC则可以以毫秒为单位计算。以下源代码获取表示从现在开始150毫秒后时间的值

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 150ull * NSEC_PER_SEC);

dispatch_walltime函数由POSIX中使用的struct timespec类型的时间得到dispatch_time_t类型的值。dispatch_time函数通常用于计算相对时间,而dispatch_walltime函数用于计算绝对时间。例如在dispatch_after函数中想指定2011年11月11日11时11分11秒这一绝对时间的情况,这可作为粗略的闹钟功能使用。

dispatch_once

  • 一次性代码(只执行一次):dispatch_once

我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 函数。使用dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

下面这种经常出现的用来进行初始化的源代码可通过dispatch_once函数简化。

- (void)once {
	static int initialized = NO;
    
    if (initialized == NO) {
        /*
         *初始化
         */   
        initialized = YES;
    }
}

如果使用dispatch_once函数,则源代码写为:

/**
 * 一次性代码(只执行一次)dispatch_once
 */
- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行1次的代码(这里面默认是线程安全的)
    });
}
  • 源代码看起来没有太大的变化,但是通过dispatch_once函数,该源代码即使在多线程环境下执行,也可保证百分之百安全。

之前的源代码在大多数情况下也是安全的。但是在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。而用dispatch_once函数初始化就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。

dispatch_group

  • GCD 的队列组:dispatch_group

在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理,这种情况会经常出现。只使用一个Serial Dispatch Queue时,只要将想执行的处理全部追加到该Serial Dispatch Queue中并在最后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue时或同时使用多个Dispatch Queue时,源代码就会变得颇为复杂。

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。

  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enterdispatch_group_leave 组合 来实现dispatch_group_async
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。

dispatch_group_notify

  • 监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
/**
 * 队列组 dispatch_group_notify
 */
- (void)groupNotify {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    dispatch_group_t group =  dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
        NSLog(@"group---end");
    });
}

运行结果:

请添加图片描述

dispatch_group_notify相关代码运行输出结果可以看出:

  • 当所有任务都执行完成之后,才执行dispatch_group_notify block 中的任务。

dispatch_group_wait

  • 暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
/**
 * 队列组 dispatch_group_wait
 */
- (void)groupWait {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"group---end");
}

运行结果:

请添加图片描述

dispatch_group_wait相关代码运行输出结果可以看出:

  • 当所有任务执行完成之后,才执行 dispatch_group_wait 之后的操作。但是,使用dispatch_group_wait 会阻塞当前线程。

dispatch_group_enter 、 dispatch_group_leave

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。

group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。

/**
 * 队列组 dispatch_group_enter、dispatch_group_leave
 */
- (void)groupEnterAndLeave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
        dispatch_group_leave(group);
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程.
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
        NSLog(@"group---end");
    });
    
}

运行结果:

请添加图片描述

dispatch_group_enterdispatch_group_leave相关代码运行结果中可以看出:

  • 当所有任务执行完成之后,才执行 dispatch_group_notify 中的任务。这里的dispatch_group_enterdispatch_group_leave组合,其实等同于dispatch_group_async

dispatch_apply

通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_applydispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

我们可以利用异步队列同时遍历。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个数字。

/*
 * 快速迭代方法 dispatch_apply
 */

- (void)apply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"apply---begin");
    dispatch_apply(6, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
    
}

运行结果:

请添加图片描述

从dispatch_apply相关代码执行结果中可以看出:

  • 0~5 打印顺序不定,最后打印了 apply---end
  • 因为是在并发队列中异步队执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是apply---end一定在最后执行。这是因为dispatch_apply函数会等待全部任务执行完毕。

dispatch_barrier_async

  • GCD 栅栏方法:dispatch_barrier_async

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。

  • dispatch_barrier_async函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。具体如下图所示:

请添加图片描述

/**
 * 栅栏方法 dispatch_barrier_async
 */
- (void)barrier {
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_barrier_async(queue, ^{
        // 追加任务 barrier
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务3
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    dispatch_async(queue, ^{
        // 追加任务4
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
}

运行结果:

请添加图片描述

dispatch_barrier_async相关代码执行结果中可以看出:

  • 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。

dispatch_semaphore

  • GCD 信号量:dispatch_semaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。Dispatch Semaphore 提供了三个函数。

  • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

我们来思考一下这种情况:不考虑顺序,将所有数据追加到NSMutableArray中。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    for (int i = 0; i < 100000; ++i) {
        dispatch_async(queue, ^{
            [array addObject:[NSNumber numberWithInt:i]];
        });
    }

因为该源代码使用Global Dispatch Queue更新NSMutbleArray类对象,所以执行后由内存错误导致应用程序异常结束的概率很高。此时应使用Dispatch Semaphore

  • Dispatch Semaphore本来使用的是更细粒度的对象,不过本书还是使用该源代码对Dispatch Semphore进行说明。
  • Dispatch Semaphore是持有技术的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或大于1时,减去1而不等待。

什么是细粒度/粗粒度?

细粒度(fine-grained):粒度似乎根据项目模块划分的细致程度区分的,一个项目模块(或子模块)分得越多,每个模块(或子模块)越小,负责的工作越细,就说是细粒度。

粗粒度(coarse-grained):相对于细粒度而言,一个项目模块(或子模块)分得越少,每个模块(或子模块)越大,负责的工作越泛,就说是粗粒度。

粗粒度和细粒度是一个相对的概念,定义这个概念主要是出于重用的目的,比如:类的设计,为尽可能的重用则采用细粒度的设计模式,将一个复杂的类拆分成高度重用的细化的类。粗粒度和细粒度这个概念也出现在数据库的课本中,这里就不赘述了。

下面介绍一下使用方法。通过dispatch_semap_creat函数生成Dispatch Semaphore

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
  • 参数表示计数的初始值。本例将计数值初始化为“1”。从函数名称中含有的creat可以看出,该函数与Dispatch QueueDispatch Group一样,必须通过dispatch_release函数释放。当然,也可通过dispatch_retain函数持有。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

dispatch_semphore_wait函数等待Dispatch Semaphore的计数值达到大于或等于1。当计数值大于等于1,或者在待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait函数返回。第二个参数与dispatch_group_wait函数等相同,有dispatch_time_t类型值指定等待时间。该例的参数意味着永久等待。另外,dispatch_group_wait函数的返回值也与dispatch_group_wait函数相同。可像以下源代码这样,通过返回值进行分支处理。

	dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull*NSEC_PER_SEC);
    long result = dispatch_semaphore_wait(semaphore, time);
    
    if (result == 0) {
          /*
           * 由于dispatch Semaphore的计数达到大于等于1
           * 或者在待机中的指定时间内
           * Dispatch Semaphore的计数值达到大于等于1
           * 所以Dispatch SEMAPHORE的计数值减去1
           *
           * 可执行需要进行排他控制的处理
           */
           
    } else {
          /*
           * 由于Dispatch Semaphore的计数值为0
           * 因此在达到指定时间为止待机
           */
    }
  • dispatch_semaphore_wait函数返回0时,可安全的执行需要进行排他控制的处理。该处理结束时通过dispatch_semaphore_signal函数将Dispatch Semaphore的计数值加1.

我们在前面的源代码中实际使用Dispatch Semphore看看。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
     /*
      * 生成Dispatch Semaphore
      * Dispatch Semaphore的计数初始值设定为“1”
      *
      * 保证可访问NSMutableArray类对象的线程
      * 同时只能有1个
      */
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (int i = 0; i < 100000; ++i) {
        dispatch_async(queue, ^{
            
           	  /*
               * 等待Dispatch Semaphore
               * 一直等待,直到Dispatch Semaphore的计数值达到大于等于1
               *
               */
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
               /*
                * 由于Dispatch Semphore的计数值达到大于等于1
                * 所以将Dispatch Semaphore的计数值减去1,
                * dispatch_semaphore_wait函数执行返回
                *
                * 即执行到此时时的
                * Dispatch Semaphore的计数值恒为“0”
                *
                * 由于可访问NSMutableArray类对象的线程
                * 只有1个
                * 因此可安全地进行更新
                */
            
            [array addObject:[NSNumber numberWithInt:i]];
            
               /*
                * 排他控制处理结束
                * 所以通过dispatch_semaphore_signal函数
                * 将Dispatch Semaphore的计数值增加的线程
                *
                * 如果有通过dispatch_semaphore_wait函数
                * 等待Dispatch Semaphore的计数值增加的线程
                * 就由最先等待的线程执行
                */
            
            dispatch_semaphore_signal(semaphore);
        });
    }
    
     /*
      * 如果使用结束,需要如下选择
      * 释放Dispatch Semaphore
      *
      * dispatch_release(semaphore)
      */

在没有Serial Dispatch Queuedispatch_barrier_async函数那么大粒度且一部分处理需要进行排他控制的情况下,Dispatch Semaphore便可发挥威力。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值