• [iOS]-GCD(Grand Central Dispatch)


    目录:

    参考的博客:

    iOS八股文(十一)多线程之GCD
    iOS八股文(十二)GCD之函数和死锁源码浅析
    iOS八股文(十三)GCD函数解析(栅栏、信号量、一次性、调度组)
    [iOS开发]GCD
    iOS多线程:『GCD』详尽总结

    GCD简介

    GCD是异步执行任务的技术之一。将应用程序中记述的线程管理用的代码在系统级中实现

    我们为什么要使用GCD

    • GCD可用于多核的并行运算
    • GCD会自动利用更多的CPU内核
    • GCD会自动管理线程的生命周期
    • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理的代码

    GCD任务和队列

    任务

    任务就是执行操作,也就是我们在线程中执行的那段代码。在GCD中是放在Block中的。
    具体对任务的执行有两种方式:同步执行异步执行,主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力

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

    一个简单的例子: 你要给小张和小王打电话,同步执行就是:你打电话给小张的时候,不能打给小王,只有等到你给小张打完电话之后你才能打给小王(等待任务执行结束),并且只能用当前的电话(不具备开启新线程的能力)。异步执行就是:你打电话给小张的时候,不用等着和小张结束(不用等待任务执行结束),还能同时打给小王。而且除了当前电话,你还可以使用其他的一个或多个电话(具备开启新线程的能力)。

    注意: ==异步执行(async)==虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

    队列(Dispatch Queue)

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

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

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

    注意: 并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

    GCD的使用步骤

    GCD 的使用步骤很简单,只有两步:

    1. 创建一个队列(串行队列或并发队列)
    2. 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)

    下边来看看队列的创建方法 / 获取方法,以及任务的创建方法。

    队列的创建方法 / 获取方法

    可以使用 dispatch_queue_create 方法来创建队列。该方法需要传入两个参数:

    • 第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
    • 第二个参数用来识别是串行队列还是并发队列。
      • DISPATCH_QUEUE_SERIAL 表示串行队列
      • DISPATCH_QUEUE_CONCURRENT 表示并发队列
    // 串行队列的创建方法
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
    // 并发队列的创建方法
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    • 1
    • 2
    • 3
    • 4

    对于串行队列GCD 默认提供了:主队列(Main Dispatch Queue)。

    • 所有放在主队列中的任务,都会放到主线程中执行
    • 可使用 dispatch_get_main_queue() 方法获得主队列

    主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象

    // 主队列的获取方法
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    • 1
    • 2

    对于并发队列GCD 默认提供了 全局并发队列(Global Dispatch Queue)

    • 可以使用 dispatch_get_global_queue 方法来获取全局并发队列。需要传入两个参数。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用 0 即可。
    // 全局并发队列的获取方法
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    • 1
    • 2

    任务的创建方法

    GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async

    // 同步执行任务创建方法
    dispatch_sync(queue, ^{
        // 这里放同步执行任务代码
    });
    // 异步执行任务创建方法
    dispatch_async(queue, ^{
        // 这里放异步执行任务代码
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    虽然使用 GCD 只需两步,但是既然我们有两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:

    1. 同步执行 + 并发队列
    2. 异步执行 + 并发队列
    3. 同步执行 + 串行队列
    4. 异步执行 + 串行队列

    实际上,刚才还说了两种默认队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是当前代码默认放在主队列中,所以主队列很有必要专门来研究一下,所以我们就又多了两种组合方式。这样就有六种不同的组合方式了:

    1. 同步执行 + 主队列
    2. 异步执行 + 主队列

    不同组合方式各有什么不同呢?

    任务和队列不同组合方式的区别

    我们先来考虑最基本的使用,也就是当前线程为 主线程 的环境下,不同队列+不同任务 简单组合使用的不同区别。暂时不考虑 队列中嵌套队列 的这种复杂情况:

    主线程中,不同队列+不同任务简单组合的区别:

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

    注意: 在主线程中调用主队列+同步执行会导致死锁问题。 这是因为 主队列中追加的同步任务 和 主线程本身的任务 两者之间相互等待,阻塞了 主队列,最终造成了主队列所在的线程(主线程)死锁问题,原因是造成了主线程等待主线程执行完之后才去执行,所以就会一直等待下去,例子如下:
    请添加图片描述
    而如果我们在 其他线程 调用 主队列+同步执行,则不会阻塞 主队列,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务,因为是其他线程等待主线程执行完之后才开始执行自己的线程,所以就只需要照常等待主线程的执行即可。

    队列嵌套情况下,不同组合方式区别

    除了上边提到的主线程中调用主队列+同步执行会导致死锁问题。实际在使用串行队列的时候,也可能出现阻塞串行队列所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。

    比如下面代码这样:在异步执行+串行队列的任务中,又嵌套了当前的串行队列,然后进行同步执行

    dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{    // 异步执行 + 串行队列
        dispatch_sync(queue, ^{  // 同步执行 + 当前串行队列
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    请添加图片描述
    我们可以看到执行上面的代码会导致 串行队列中追加的任务串行队列中原有的任务 两者之间相互等待,阻塞了串行队列,最终造成了串行队列所在的线程(子线程)死锁问题。

    主队列造成死锁也是基于这个原因,所以,这也进一步说明了主队列其实并不特殊。

    队列中嵌套队列这种复杂情况,我们先抛出一个结论,后面再去讲
    不同队列+不同任务 组合,以及 队列中嵌套队列使用的区别:
    请添加图片描述

    关于不同队列和不同任务的形象理解

    假设我们现在有5个人要穿过一个门禁,这个门禁总共有10个入口,管理员可以决定同一时间打开几个门,可以决定同一时间让一个人单独通过还是多个人一起通过。默认情况下,管理员只开启一个入口,且一个通道一次仅仅通过一个人。

    这个例子中,人当作任务,管理员当作系统,入口代表线程。

    5个人表示5个任务
    10个人表示10条线程

    • 串行队列就是5个人排成一支长队
    • 并发队列就是5个人拍成不止一支队伍,可能2,3队
    • 同步任务就是管理员只开启了一个入口(当前线程)
    • 异步任务就是管理员同时开启了多个入口(当前线程+新开的线程)

    • 异步+并发: 管理员开了多个入口(假设为3个),5个人排成了多只队伍(假设3个队伍),这样5个人就可以3个人同时一起穿过门禁
    • 同步+并发: 同步这就开了一个入口,虽然并发执行有多只队伍,但是一个入口一次只能通过1个人,所以只好一个一个的走过去,表现的结果就是顺利通过入口

    所以换成GCD来说:

    • 异步+并发:系统开启了多个线程,任务可以多个同时运行
    • 同步+并发:系统默认开了一个主线程,没有开启子线程,虽然处于并发队列中,但也只能一个接一个执行

    GCD 的基本使用

    先来讲讲并发队列的两种执行方式

    同步执行 + 并发队列

    特点: 在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务(一个一个排队执行任务)

    /**
     * 同步执行 + 并发队列
     * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
     */
    - (void)syncConcurrent {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"syncConcurrent---begin");
        
        dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_sync(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_sync(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_sync(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"syncConcurrent---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    打印结果如下:
    请添加图片描述
    同步执行 + 并发队列 中可看到:

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

    异步执行 + 并发队列

    特点: 可以开启多个线程,任务交替(同时)执行

    /**
     * 异步执行 + 并发队列
     * 特点:可以开启多个线程,任务交替(同时)执行。
     */
    - (void)asyncConcurrent {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"asyncConcurrent---begin");
        
        dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_async(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_async(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"asyncConcurrent---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行结果如下:
    请添加图片描述
    异步执行 + 并发队列 中可以看出:

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

    注意: 如果我们在main函数程序快要return 0终止之前添加的这三个异步执行的任务,那么就有可能在截止程序终止的时候只完成了其一的任务其二的任务三个都完成没有一个完成,且完成的任务的顺序是不确定。

    接下来再来讲讲串行队列的两种执行方式。

    同步执行 + 串行队列

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

    /**
     * 同步执行 + 串行队列
     * 特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
     */
    - (void)syncSerial {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"syncSerial---begin");
        
        dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
        
        dispatch_sync(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_sync(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_sync(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"syncSerial---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    运行结果如下:
    请添加图片描述
    同步执行 + 串行队列 可以看到:

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

    异步执行 + 串行队列

    特点: 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务

    /**
     * 异步执行 + 串行队列
     * 特点:会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务。
     */
    - (void)asyncSerial {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"asyncSerial---begin");
        
        dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_async(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_async(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"asyncSerial---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    运行结果如下:
    请添加图片描述
    异步执行 + 串行队列 可以看到:

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

    下边讲讲刚才我们提到过的:主队列

    • 主队列: GCD 默认提供的 串行队列
      • 默认情况下,平常所写代码是直接放在主队列中的
      • 所有放在主队列中的任务,都会放到主线程中执行
      • 可使用 dispatch_get_main_queue() 获得主队列

    我们再来看看主队列的两种组合方式

    同步执行 + 主队列

    同步执行 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会死锁

    在主线程中调用 “同步执行 + 主队列”

    特点: 互相等待卡住不可行

    /**
     * 同步执行 + 主队列
     * 特点(主线程调用):互等卡主不执行。
     * 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
     */
    - (void)syncMain {
        
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"syncMain---begin");
        
        dispatch_queue_t queue = dispatch_get_main_queue();
        
        dispatch_sync(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_sync(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_sync(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"syncMain---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    运行结果:
    请添加图片描述请添加图片描述
    主线程中使用 同步执行 + 主队列 可以发现:

    • 追加到主线程任务 1、任务 2、任务 3 都不再执行了,而且 syncMain---end 也没有打印,在 XCode 9 及以上版本上还会直接报崩溃。这是为什么呢?

    这是因为我们在主线程中执行 syncMain 方法,相当于把 syncMain 任务放到了主线程的队列中。而 同步执行 会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务 1 追加到主队列中,任务 1 就在等待主线程处理完 syncMain 任务。而syncMain 任务需要等待 任务 1 执行完毕,才能接着执行,所以就形成了互相等待,造成死锁。

    那么,现在的情况就是 syncMain 任务任务 1 都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,也就导致后面的程序都在等待被执行,结果就是由于一直在等待 syncMain---end 也没有打印

    要是如果不在主线程中调用,而在其他线程中调用会如何呢?

    在其他线程中调用 “同步执行 + 主队列”

    特点: 不会开启新线程,执行完一个任务,再执行下一个任务

    // 使用 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
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_sync(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_sync(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"syncMain---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    运行结果如下:
    请添加图片描述
    在其他线程中使用 同步执行 + 主队列 可看到:

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

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

    因为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
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_async(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        dispatch_async(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"asyncMain---end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行结果如下:
    请添加图片描述
    异步执行 + 主队列 可以看到:

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

    这些麻烦的不同队列+不同任务总算是搞懂啦,接着我们来了解一下 GCD 线程间的通信

    GCD 线程间的通信

    iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯

    /**
     * 线程间通信
     */
    - (void)communication {
        // 获取全局并发队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        // 获取主队列
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        
        dispatch_async(queue, ^{
            // 异步追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            
            // 回到主线程
            dispatch_async(mainQueue, ^{
                // 追加在主线程中执行的任务
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    打印结果如下:
    请添加图片描述
    可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。

    GCD 的其他方法

    栅栏方法:dispatch_barrier_async

    • 我们有时候需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,当然操作组里也可以包含一个或者多个任务
    • 这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏
    • dispatch_barrier_async方法会等待前边追加到并发队列中的任务全部执行完毕后,再将制定的任务追加到该异步队列中。然后在dispatch_barrier_async方法追加的任务执行完毕之后,接着追加任务到该异步队列并开始执行,大佬博客中的示意图非常的形象,具体图示如下:
      在这里插入图片描述

    栅栏方法的代码使用样例如下:

    - (void) barrier {
        dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_CONCURRENT);
            
            dispatch_async(queue, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_async(queue, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queue, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    运行结果如下:
    请添加图片描述
    在执行完栅栏前面的操作之后执行栅栏操作,最后再执行栅栏后面的操作

    GCD中同步栅栏和异步栅栏

    我们之前考虑异步栅栏+单一队列的时候栅栏只作用于同一队列

    那么对于身处不同队列的任务又有什么样的拦截作用呢?

    对于重要的栅栏方法部分,我们将各种情况都实验一下:

    异步栅栏+单一串行队列:

    (由于异步执行+串行队列本身就是在创建的唯一一个新线程里按任务添加顺序排队执行,所以其实在这种情况下添加栅栏是没有意义的)

    - (void) asyncBarrierAndOneSerial {
        dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_SERIAL);
            
            dispatch_async(queue, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_async(queue, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queue, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果如下:
    请添加图片描述
    我们可以看到是非常规矩的按添加任务的顺序一个一个排队来执行

    异步栅栏+单一并行队列:

    (该种情况上方已经讲述过了)

    - (void) asyncBarrierAndOneConcurrent {
        dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_CONCURRENT);
            
            dispatch_async(queue, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_async(queue, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queue, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果:
    请添加图片描述
    可以看到栅栏的确是将前后添加的任务分割开了,先执行栅栏前的任务,再执行栅栏中的任务,最后再执行栅栏之后的任务,不过由于是在一个异步队列中,异步队列会开启新线程,所以我们在栅栏前的任务组中任务的执行结束的顺序栅栏后的任务组中的任务执行结束的顺序是不确定的。

    同步栅栏+单一串行队列:

    - (void) syncBarrierAndOneSerial {  
        dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_SERIAL);
            
            dispatch_async(queue, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_sync(queue, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queue, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果:
    请添加图片描述
    我们可以看到在串行队列中无论是同步执行还是异步执行,都是排好队一个一个按顺序来执行的。

    同步栅栏+单一并行队列:

    - (void) syncBarrierAndOneConcurrent {
        dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_CONCURRENT);
            
            dispatch_async(queue, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_sync(queue, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queue, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queue, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果如下:
    请添加图片描述
    实际的运行结果是栅栏前的任务组(也就是任务1任务2),在程序开始执行两秒之后同时打印了结果,接着两秒的时间单独执行了栅栏中的方法,最后两秒时间同时执行了栅栏后的任务组(也就是任务3任务4),而且由于栅栏前后的任务组中的任务都是在并行队列中异步执行,所以执行结束的顺序是不确定的。

    异步栅栏+多个串行队列:

    - (void) asyncBarrierAndSerials {
        dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_SERIAL);
        
        
            dispatch_async(queueFirst, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueSecond, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_async(queueThird, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queueFourth, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueFifth, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    第一次运行结果如下:
    请添加图片描述
    第二次运行结果如下:
    请添加图片描述
    我们从上方两次的运行结果中可以看到,异步栅栏+多个串行队列的情况下每个任务都是几乎同时执行的,五个任务执行的结束时间都是完全随机的,此时的栅栏也就失去了该有的意义。

    异步栅栏+多个并行队列:

    异步栅栏+多个串行队列情况的各任务执行结束时间都是完全随机的,所以异步栅栏+多个并行队列更是可想而知,肯定也是完全随机的。

    - (void) asyncBarrierAndConcurrents {
        dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_CONCURRENT);
        
        
            dispatch_async(queueFirst, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueSecond, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_async(queueThird, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queueFourth, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueFifth, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    第一次运行结果:
    请添加图片描述
    第二次运行结果:
    请添加图片描述
    从上方两次的执行结果我们可以看出:各个任务的执行结束时间都是完全随机的,跟异步栅栏+多个串行队列时的情况非常类似。

    同步栅栏+多个串行队列:

    - (void) syncBarrierAndSerials {
        dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_SERIAL);
        
        
            dispatch_async(queueFirst, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueSecond, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_sync(queueThird, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queueFourth, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueFifth, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    第一次运行结果:
    请添加图片描述
    第二次运行结果:
    请添加图片描述
    这种情况下的栅栏任务1还有任务2几乎同时执行同时先出结果的(而且每次栅栏都是第一个出结果),但是由于同步的栅栏占用了主线程,就导致栅栏后的任务3任务4只能等到栅栏中的任务执行完成之后再开始去执行。

    同步栅栏+多个并行队列:

    - (void) syncBarrierAndConcurrents {
        dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_CONCURRENT);
        
        
            dispatch_async(queueFirst, ^{
                // 追加任务 1
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueSecond, ^{
                // 追加任务 2
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
            });
            
            dispatch_barrier_sync(queueThird, ^{
                // 追加任务 barrier
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
            });
            
            dispatch_async(queueFourth, ^{
                // 追加任务 3
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
            });
            dispatch_async(queueFifth, ^{
                // 追加任务 4
                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
                NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    第一次运行结果如下:
    请添加图片描述
    第二次运行结果如下:
    请添加图片描述
    实际运行过程中,任务1任务2栅栏都是同时先开始去执行的,而且三者执行结束的时间是不确定的,然而由于栅栏占用了主线程的原因,任务3任务4只有等到栅栏执行完成之后才开始执行。

    延时执行方法:dispatch_after

    我们经常会遇到这样的需求:在指定时间(例如 3 秒)之后执行某个任务。这种情况就可以用 GCDdispatch_after 方法来实现。

    需要注意的是:dispatch_after 方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 方法是很有效的。

    /**
     * 延时执行方法 dispatch_after
     */
    - (void)after {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"asyncMain---begin");
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 2.0 秒后异步追加任务代码到主队列,并开始执行
            NSLog(@"after---%@",[NSThread currentThread]);  // 打印当前线程
        	NSLog(@"asyncMain---willEnd");
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果如下:
    请添加图片描述
    具体的运行情况是:先打印了asyncMain---begin,接着过了两秒后紧接着按顺序打印了after---<_NSMainThread: 0x60000110c900>{number = 1, name = main}asyncMain---willEnd

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

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

    /**
     * 一次性代码(只执行一次)dispatch_once
     */
    - (void)once {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            // 只执行 1 次的代码(这里面默认是线程安全的)
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    GCD 快速迭代方法:dispatch_apply

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

    如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。但是这样就体现不出快速迭代的意义了

    我们可以利用并发队列进行异步执行。比如说遍历 0~56 个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以在多个线程同时(异步)遍历多个数字。

    还有一点,无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法

    /**
     * 快速迭代方法 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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果如下:
    请添加图片描述
    因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是 apply---end 一定在最后执行。这是因为 dispatch_apply 方法会等待全部任务执行完毕

    GCD 队列组:dispatch_group

    有时候我们会有这样的需求:分别异步执行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 中,并执行任务:

    - (void)group {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_t group = dispatch_group_create();
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk0!");
        });
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk1!");
        });
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk3!");
        });
        
        //dispatch_group_notify会等到group中的处理全部结束时再开始执行
        //在group中的处理全部结束时,将第三个参数(block)追加到第二个参数所对应的queue中
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"done");
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行结果如下:
    请添加图片描述我们可以看到,由于添加到group中的队列在多线程并发时的执行结果时间是不确定的,所以打印的顺序都是随机的。

    dispatch_group_wait

    另外我们也可以使用dispatch_group_wait(group, DISPATCH_TIME_FOREVER);//第二个参数为dispatch_time_t类型,可以自定义来等待group中的处理全部结束

    dispatch_group_wait用于暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。

    如果我们不去添加dispatch_group_wait来进行等待,那么由于group中的处理本身也是异步的,所以就会在group中的处理还没有执行完时就去执行其他的任务,例子如下:

    - (void)group {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_t group = dispatch_group_create();
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk0!");
        });
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk1!");
        });
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk3!");
        });
        
    //    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //        NSLog(@"done");
    //    });
        NSLog(@"YES!!");
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        //NSLog(@"YES!!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    打印结果如下:
    请添加图片描述
    可以看到打印YES!!操作在group中的处理还没有执行完时就已经执行了

    而像下面这样:

    - (void)group {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_t group = dispatch_group_create();
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk0!");
        });
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk1!");
        });
         
        dispatch_group_async(group, queue, ^{
            NSLog(@"blk3!");
        });
        
    //    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //        NSLog(@"done");
    //    });
        
        //NSLog(@"YES!!");
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"YES!!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行结果如下:
    请添加图片描述
    可以看到就是在group中全部的处理执行完之后再执行的打印YES!!操作

    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_enterdispatch_group_leave 配和来实现向group添加操作:

    - (void)groupWithEnterAndLeave {
        // 首先 需要创建一个线程组
        dispatch_group_t group = dispatch_group_create();
        // 任务1
        dispatch_group_enter(group);
        void (^blockFirst)(int) = ^(int a){
            NSLog(@"任务%d完成!", a);
            dispatch_group_leave(group);
        };
        
        blockFirst(1);
        
        // 任务2
        dispatch_group_enter(group);
        void (^blockSecond)(int) = ^(int a){
            NSLog(@"任务%d完成!", a);
            dispatch_group_leave(group);
        };
        
        blockSecond(2);
        
        // 全部完成
        dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
            NSLog(@"全部完成");
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    运行结果如下:
    请添加图片描述
    我们可以看到:任务1任务2执行完成之后,才会执行全部完成中的任务。
    dispatch_group_enterdispatch_group_leave 相关代码运行结果中可以看出:当所有任务执行完成之后,才执行 dispatch_group_notify 中的任务。这里的dispatch_group_enterdispatch_group_leave 组合,其实等同于dispatch_group_async

    不过使用dispatch_group_enterdispatch_group_leave 需要成对出现:

    如果 dispatch_group_leave 的调用次数多于 dispatch_group_enter 的调用次数,程序会 crash,如下所示:
    请添加图片描述
    相反dispatch_group_enter 的调用次数多于dispatch_group_leave,虽然不会发生 crash , 但可能不会达到预期效果,如下所示:
    请添加图片描述
    上图中我们发现只打印了任务1完成任务2完成但是dispatch_group_notify中需要执行的操作将会一直等待。

    GCD 信号量:dispatch_semaphore

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

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

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

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

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

    Dispatch Semaphore 线程同步

    我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。

    下面我们就来利用Dispatch Semaphore实现线程同步,将一步执行任务转换为同步执行任务:

    /**
     * semaphore 线程同步
     */
    - (void)semaphoreSync {
        
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"semaphore---begin");
        
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
        __block int number = 0;
        dispatch_async(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
            
            number = 100;
            
            dispatch_semaphore_signal(semaphore);
        });
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"semaphore---end,number = %zd",number);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    运行结果如下:
    请添加图片描述
    可以看到semaphore---end 是在执行完 number = 100; 之后才打印的。而且输出结果 number100。整个的执行顺序如下:

    1. semaphore 初始创建时计数为 0
    2. 异步执行任务 1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态(后面的内容不执行,只执行我们所添加的任务1,等到dispatch_semaphore_signal操作使信号量计数>=0时线程才会恢复正常运作)
    3. 然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行
    4. 最后打印 semaphore---end,number = 100

    这样就实现了线程同步,将异步执行任务转换为同步执行任务。

    Dispatch Semaphore 线程安全和线程同步(为线程加锁)

    线程安全: 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的

    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全

    线程同步: 可理解为线程 A线程 B 一块配合,线程 A 执行到一定程度时要依靠线程B 的某个结果,于是停下来,示意 线程B 运行;线程B 依言执行,再将结果给 线程A线程A 再继续操作。

    举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

    下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题(例子借鉴自:大佬博客)。

    场景: 总共有 50 张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。

    非线程安全(不使用 semaphore)

    先来看看不考虑线程安全的代码:

    @interface ViewController ()
    
    @property (nonatomic, assign) NSInteger ticketSurplusCount;
    
    @end
    
    
    
    /**
     * 非线程安全:不使用 semaphore
     * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
     */
    - (void)initTicketStatusNotSafe {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"semaphore---begin");
        
        self.ticketSurplusCount = 50;
        
        // queue1 代表北京火车票售卖窗口
        dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
        // queue2 代表上海火车票售卖窗口
        dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
        
        __weak typeof(self) weakSelf = self;
        dispatch_async(queue1, ^{
            [weakSelf saleTicketNotSafe];
        });
        
        dispatch_async(queue2, ^{
            [weakSelf saleTicketNotSafe];
        });
    }
    
    /**
     * 售卖火车票(非线程安全)
     */
    - (void)saleTicketNotSafe {
        while (1) {
            
            if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
                self.ticketSurplusCount--;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
                [NSThread sleepForTimeInterval:0.2];
            } else { // 如果已卖完,关闭售票窗口
                NSLog(@"所有火车票均已售完");
                break;
            }
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    运行结果如下:
    请添加图片描述
    可以看到在不考虑线程安全,不使用 semaphore 的情况下,得到票数是错乱的,而且同一张票可能会发生卖两遍的情况,这样显然不符合我们的需求,所以我们需要考虑线程安全问题

    线程安全(使用 semaphore 加锁)

    考虑线程安全的代码:

    @interface ViewController ()
    
    @property (nonatomic, assign) NSInteger ticketSurplusCount;
    
    @end
    
    
    //创建一个全局信号量
    dispatch_semaphore_t semaphoreLock;
    
    /**
     * 线程安全:使用 semaphore 加锁
     * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
     */
    - (void)initTicketStatusSafe {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"semaphore---begin");
        
        semaphoreLock = dispatch_semaphore_create(1);
        
        self.ticketSurplusCount = 50;
        
        // queue1 代表北京火车票售卖窗口
        dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
        // queue2 代表上海火车票售卖窗口
        dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
        
        __weak typeof(self) weakSelf = self;
        dispatch_async(queue1, ^{
            [weakSelf saleTicketSafe];
        });
        
        dispatch_async(queue2, ^{
            [weakSelf saleTicketSafe];
        });
    }
    
    /**
     * 售卖火车票(线程安全)
     */
    - (void)saleTicketSafe {
        while (1) {
            // 相当于加锁
            dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
            
            if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
                self.ticketSurplusCount--;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
                [NSThread sleepForTimeInterval:0.2];
            } else { // 如果已卖完,关闭售票窗口
                NSLog(@"所有火车票均已售完");
                
                // 相当于解锁
                dispatch_semaphore_signal(semaphoreLock);
                break;
            }
            
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    运行结果如下:
    请添加图片描述
    思路: 此处我们采用了dispatch_semaphore 机制,买票的操作是每次异步执行的,但是如果第一张票还没卖出去第二张票已经开始卖了的话就会由于dispatch_semaphore_wait操作使得信号量计数=-1,线程就会进入等待状态,等待第一张票卖完之后的dispatch_semaphore_signal操作,这个操作会让信号量的计数=1,使得线程重写开始正常运行,开始正常执行卖第二张票的处理,以此类推,通过保护每一次的卖票从而实现整个售票流程的正确性。

    可以看出,在考虑了线程安全的情况下,使用 dispatch_semaphore 机制之后,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。

    GCD一些题目

    题目一:
    以下代码输出结果是什么?

    -(void)interviewFirst {
        dispatch_queue_t queue = dispatch_queue_create("com.demo.queue", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"test____1"); // 任务1
        dispatch_async(queue, ^{
            NSLog(@"test____2"); // 任务2
            dispatch_sync(queue, ^{
                NSLog(@"test____3"); // 任务3
            });
            NSLog(@"test____4"); // 任务4
        });
        NSLog(@"test____5"); // 任务5
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果:
    请添加图片描述
    我们可以看到输出的结果为:1、5、2、3、4。可以说这个就是答案,但是又不完全对。

    首先这段代码是在主线程中执行的,那么在主线程执行的内容我们可以分为3个部分:
    请添加图片描述
    任务1肯定是最先执行的,然后去执行第2块内容,最后执行任务5,再继续看第2块部分使用异步函数配合并发队列,那么disptch里面的block将会在一个新的线程执行。同样的在这个线程里面也可以将执行的代码分成3个部分:
    请添加图片描述
    在这块代码里面任务2会被先执行,然后再看dispatch_syns,同步函数,那么都不需要看是什么对列,任务3将会次之执行,最后就是执行任务4

    剩下的就是需要考虑任务5任务2、3、4的执行顺序了。正确答案其实是他们之间没有顺序,他们在不同的线程执行,理论上讲是可以同时执行的。而之所以打印出来任务2、3、4任务5之后,其实是在执行任务2、3、4之前,需要创建其执行的线程,创建线程需要消耗微小的时间,还有一方面原因是这块代码在主线程运行的,任务5在主线程执行,效率也会更高。

    这道题考查的是GCD中同步、异步函数和串行、并行队列的使用,使用NSLog来模拟任务执行,但正真的环境中任务的耗时程度是不一样的。

    我们假设任务5是一个耗时20微秒的任务,那么打印结果会不会不一样呢?

    -(void)interviewFirst {
        dispatch_queue_t queue = dispatch_queue_create("com.demo.queue", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"test____1"); // 任务1
        dispatch_async(queue, ^{
            NSLog(@"test____2"); // 任务2
            dispatch_sync(queue, ^{
                NSLog(@"test____3"); // 任务3
            });
            NSLog(@"test____4"); // 任务4
        });
        //执行任务5前先等20微秒
        usleep(120);
        NSLog(@"test____5"); // 任务5
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    任务5之前usleep 120微秒模拟耗时任务。

    注意: usleepsleep的区别只是单位不一样,usleep的单位是微秒sleep的单位是

    现在的运行结果如下:
    请添加图片描述

    可以看到这时候顺序变成了 1、2、5、3、4,正如我们分析结果,52、3、4 是没有顺序的。如果200微妙,运行结果还会是不一样的。

    现在将usleep中的等待时间换成200微秒,运行结果如下:
    请添加图片描述
    可以看到 5 居然走到了所有的 2、3、4 之后。

    所以这道题面试题的正确答案完整版是: 先执行151后执行,2、3、4顺序执行,52、3、4是没有顺序。

    题目二:
    以下代码输出结果是什么?

    -(void)interviewSecond {
        dispatch_queue_t queue = dispatch_queue_create("com.demo.queue", DISPATCH_QUEUE_SERIAL);
        NSLog(@"test____1"); // 任务1
        dispatch_async(queue, ^{
            NSLog(@"test____2"); // 任务2
            dispatch_sync(queue, ^{
                NSLog(@"test____3"); // 任务3
            });
            NSLog(@"test____4"); // 任务4
        });
        NSLog(@"test____5"); // 任务5
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这道题的答案应该比较显而易见

    运行结果就是:
    请添加图片描述
    可以看到是崩溃了,原因也非常简单:使用同步函数当前串行队列中添加任务,会产生死锁。注意三个关键字,GCD发生死锁的充分条件。

    同步异步和串行并发队列的搭配:

    同步异步和串行并发队列的组合情况如下:

    串行队列并发队列
    同步函数可能死锁不开启新线程
    异步函数开启新线程开启新线程

    在组合的时候,还需要考虑的因素还有当前代码的执行是在哪个队列之中,之前也有讲到:使用同步函数当前 串行队列中添加任务,会产生死锁,如果在别的串行队列去添加任务是不会产生死锁的。例如:

    //在主线程中执行的
    - (void)interviewSecond {
        dispatch_queue_t queue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL);
        NSLog(@"---任务1");
        dispatch_sync(queue, ^{
            NSLog(@"---任务2");
        });
        NSLog(@"---任务3");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这段代码就是同步函数串行队列的时候,但不会造成死锁(在主线程中执行)

    第三题:

    下列代码的打印结果是什么?

    //经典面试题
    - (void)interviewThird {
        //全局队列
        dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
        __block int a = 0;
        while (a < 100) {
            dispatch_async(globQueue, ^{
    //            NSLog(@"内部: %d  - %@",a,[NSThread currentThread]);
                a++;
            });
        };
        NSLog(@"外部打印_____ %d",a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    首先结果只有3个等于100大于100大于等于100等于100的情况肯定是有可能发生的。所以就看有没有可能发生大于100的情况。代码走出while循环的时候a=100,就看在NSLog打印的时候还回不回异步执行a++了。再看a++的操作是在其他线程异步完成的(异步函数+全局队列),也就是说,有可能来到了下一次循环的while判断,上一次循环的a++还没有执行。所以a++执行的次数应该是 大于等于100 的。

    实际第一次运行结果如下:
    请添加图片描述
    实际第二次运行结果如下:
    请添加图片描述
    可以看到实际的执行结果就是 a大于等于100

    题目四:

    这道题是第三题的变种,问这段代码打印结果是多少?

    - (void)interviewFourth {
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        __block int a = 0;
        for (int i = 0; i < 100; i++) {
            dispatch_async(globalQueue, ^{
                a++;
            });
        }
        NSLog(@"外部答应_____ %d",a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这题是a++只有100次,就看在NSLog执行的时候这100a++是否都执行完了。同样是异步执行,极限情况下是有可能执行完成的,这样打印的就刚好是100,如果有没有执行完的a++,那么打印出来的值有可能小于100,所以极限最小打印值就是0

    实际第一次运行结果如下:
    请添加图片描述
    也许是由于我们的NSLog操作在主线程,效率比全局的并行队列要高不少,所以反复执行的结果都是0,于是我们就在NSLog前面添加线程休眠时长,给我们的并行执行的a++操作争取时间:

    - (void)interviewFourth {
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        __block int a = 0;
        for (int i = 0; i < 100; i++) {
            dispatch_async(globalQueue, ^{
                a++;
            });
        }
        usleep(500);
        NSLog(@"外部答应_____ %d",a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    实际第二次运行结果如下:
    请添加图片描述

    可以看到还是没有达到最大极限值100,所以我们再加大休眠时长为1000微秒

    - (void)interviewFourth {
        dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
        __block int a = 0;
        for (int i = 0; i < 100; i++) {
            dispatch_async(globalQueue, ^{
                a++;
            });
        }
        usleep(1000);
        NSLog(@"外部答应_____ %d",a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    实际第三次运行结果如下:
    请添加图片描述
    可以看到,三次的运行结果都非常符合我们的预期结果。

    下面我们来讲解一下GCD部分内容的源码实现相关内容:

    GCD部分源码

    串行队列和并发队列的源码解析

    在我们开发中,使用队列的时候,苹果给我们给了3个获取队列的api

    - (void)test01 {
        //主队列
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        //全局并发队列
        dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
        //自己创建的串行队列
        dispatch_queue_t normalQueue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL);
        NSLog(@"%@",mainQueue);
        NSLog(@"%@",globQueue);
        NSLog(@"%@",normalQueue);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们可以打开dispatch源码找到dispatch_queue_createapi实现部分,来一探究竟:

    dispatch_queue_t
    dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
    {
    	return _dispatch_lane_create_with_target(label, attr,
    			DISPATCH_TARGET_QUEUE_DEFAULT, true);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到是调用_dispatch_lane_create_with_target并添加2个默认参数实现的,找到对应实现:

    DISPATCH_NOINLINE
    static dispatch_queue_t
    _dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
    		dispatch_queue_t tq, bool legacy)
    {
    	//把我们传入的DISPATCH_QUEUE_SERIAL或者DISPATCH_QUEUE_CONCURRENT参数进行封装,封装成了dqai
    	dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
    
    	//
    	// Step 1: Normalize arguments (qos, overcommit, tq)
    	//
    
    	dispatch_qos_t qos = dqai.dqai_qos;
    #if !HAVE_PTHREAD_WORKQUEUE_QOS
    	if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
    		dqai.dqai_qos = qos = DISPATCH_QOS_USER_INITIATED;
    	}
    	if (qos == DISPATCH_QOS_MAINTENANCE) {
    		dqai.dqai_qos = qos = DISPATCH_QOS_BACKGROUND;
    	}
    #endif // !HAVE_PTHREAD_WORKQUEUE_QOS
    	后续代码省略...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    可以看到这个方法里面,把我们传入的DISPATCH_QUEUE_SERIAL或者DISPATCH_QUEUE_CONCURRENT参数进行封装,封装成了dqai。我们可以大致看看封装的实现:

    dispatch_queue_attr_info_t
    _dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
    {
    	dispatch_queue_attr_info_t dqai = { };
    	//如果为空,直接返回
    	if (!dqa) return dqai;
    
    #if DISPATCH_VARIANT_STATIC
    	if (dqa == &_dispatch_queue_attr_concurrent) {
    		//dqai里面有个dqai_concurrent的属性,代表是否是并发
    		dqai.dqai_concurrent = true;
    		return dqai;
    	}
    #endif
    
    	if (dqa < _dispatch_queue_attrs ||
    			dqa >= &_dispatch_queue_attrs[DISPATCH_QUEUE_ATTR_COUNT]) {
    #ifndef __APPLE__
    		if (memcmp(dqa, &_dispatch_queue_attrs[0],
    				sizeof(struct dispatch_queue_attr_s)) == 0) {
    			dqa = (dispatch_queue_attr_t)&_dispatch_queue_attrs[0];
    		} else
    #endif // __APPLE__
    		DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
    	}
    
    	size_t idx = (size_t)(dqa - _dispatch_queue_attrs);
    
    	dqai.dqai_inactive = (idx % DISPATCH_QUEUE_ATTR_INACTIVE_COUNT);
    	idx /= DISPATCH_QUEUE_ATTR_INACTIVE_COUNT;
    	
    	//又调用了dqai_concurrent属性
    	dqai.dqai_concurrent = !(idx % DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT);
    	idx /= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT;
    
    	dqai.dqai_relpri = -(int)(idx % DISPATCH_QUEUE_ATTR_PRIO_COUNT);
    	idx /= DISPATCH_QUEUE_ATTR_PRIO_COUNT;
    
    	dqai.dqai_qos = idx % DISPATCH_QUEUE_ATTR_QOS_COUNT;
    	idx /= DISPATCH_QUEUE_ATTR_QOS_COUNT;
    
    	dqai.dqai_autorelease_frequency =
    			idx % DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;
    	idx /= DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT;
    
    	dqai.dqai_overcommit = idx % DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;
    	idx /= DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT;
    
    	return dqai;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    从上方我们可以获取到2个有用的点,dqai里面有个dqai_concurrent的属性,顾名思义是代表是否是并发,那么默认的就是串行。

    我们在继续看如何根据dqai创建队列的(在_dispatch_lane_create_with_target方法中):

    	上方代码省略...
    	dispatch_lane_t dq = _dispatch_object_alloc(vtable,
    			sizeof(struct dispatch_lane_s));
    	//下面这个初始化方法的第三个参数,如果是并发就传入DISPATCH_QUEUE_WIDTH_MAX,如果是串行就传入1		
    	_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
    			DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
    			(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
    
    	dq->dq_label = label;
    	dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
    			dqai.dqai_relpri);
    	if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
    		dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
    	}
    	if (!dqai.dqai_inactive) {
    		_dispatch_queue_priority_inherit_from_target(dq, tq);
    		_dispatch_lane_inherit_wlh_from_target(dq, tq);
    	}
    	_dispatch_retain(tq);
    	dq->do_targetq = tq;
    	_dispatch_object_debug(dq, "%s", __func__);
    	return _dispatch_trace_queue_create(dq)._dq;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    接着我们来看一下DISPATCH_QUEUE_WIDTH_MAX的宏定义:

    #define DISPATCH_QUEUE_WIDTH_FULL_BIT		0x0020000000000000ull
    #define DISPATCH_QUEUE_WIDTH_FULL			0x1000ull
    #define DISPATCH_QUEUE_WIDTH_POOL (DISPATCH_QUEUE_WIDTH_FULL - 1)
    //利用第二行的宏定义的值进行计算得到DISPATCH_QUEUE_WIDTH_MAX
    #define DISPATCH_QUEUE_WIDTH_MAX  (DISPATCH_QUEUE_WIDTH_FULL - 2)
    #define DISPATCH_QUEUE_USES_REDIRECTION(width) \
    		({ uint16_t _width = (width); \
    		_width > 1 && _width < DISPATCH_QUEUE_WIDTH_POOL; })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以计算DISPATCH_QUEUE_WIDTH_MAX的结果是14

    我们再看_dispatch_queue_init函数的内部实现:

    //该方法里面主要注意一下width相关的内容
    static inline dispatch_queue_class_t
    _dispatch_queue_init(dispatch_queue_class_t dqu, dispatch_queue_flags_t dqf,
    		uint16_t width, uint64_t initial_state_bits)
    {
    	uint64_t dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(width);
    	//让dq指向dqu的_dq属性,然后用dq对_dq进行操作和设定,最后返回dqu
    	dispatch_queue_t dq = dqu._dq;
    
    	dispatch_assert((initial_state_bits & ~(DISPATCH_QUEUE_ROLE_MASK |
    			DISPATCH_QUEUE_INACTIVE)) == 0);
    
    	if (initial_state_bits & DISPATCH_QUEUE_INACTIVE) {
    		dq->do_ref_cnt += 2; // rdar://8181908 see _dispatch_lane_resume
    		if (dx_metatype(dq) == _DISPATCH_SOURCE_TYPE) {
    			dq->do_ref_cnt++; // released when DSF_DELETED is set
    		}
    	}
    
    	dq_state |= initial_state_bits;
    	dq->do_next = DISPATCH_OBJECT_LISTLESS;
    	//可以看到我们的width最后变成了DQF_WIDTH(width)
    	dqf |= DQF_WIDTH(width);
    	os_atomic_store2o(dq, dq_atomic_flags, dqf, relaxed);
    	dq->dq_state = dq_state;
    	dq->dq_serialnum =
    			os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
    	return dqu;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    可以看到如果width最后变成了DQF_WIDTH(width)

    接下来我们看看主队列的实现:

    DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_CONST DISPATCH_NOTHROW
    dispatch_queue_main_t
    dispatch_get_main_queue(void)
    {
    	return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到需要用到一个宏定义的函数,并且传入了2个参数,其中_dispatch_main_q是全局变量,定义如下:

    struct dispatch_queue_static_s _dispatch_main_q = {
    	DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
    #if !DISPATCH_USE_RESOLVERS
    	.do_targetq = _dispatch_get_default_queue(true),
    #endif
    	.dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
    			DISPATCH_QUEUE_ROLE_BASE_ANON,
    	.dq_label = "com.apple.main-thread",
    	.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
    	.dq_serialnum = 1,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里我们再次看到了DQF_WIDTH(1)

    根据源码里面的信息,我们可以得知,串行队列和并发队列最根本的区别就是DQF_WIDTH不同,串行队列的为1。这个width可以抽象的理解为队列出口的宽度。可以把串行队列想成一个单向单车道,把任务想成一辆辆车子,车子通过的时候必须一辆一辆按顺序通过;而并发队列可以想成单向多车道,有多个出口,车子可以并行通过。

    当然也可以通过下图理解:
    在这里插入图片描述

  • 相关阅读:
    部署LVS-NAT群集实验 待续。。
    全景环视开启「第二曲线」
    重新整理 .net core 实践篇 ———— linux上性能排查 [外篇]
    yii1 使用memcache 缓存不更新、过期时间不生效
    爬虫基础入门
    11月9日,每日信息差
    java.lang.ClassNotFoundExceptioncom.mysql.cj.jdbc.Driver解决方法
    【Vagrant】使用 Vagrant 快速创建多台 centos7 虚拟机
    java毕业设计——基于java+JSP+Tomcat的农产品销售管理系统设计与实现(毕业论文+程序源码)——农产品销售管理系统
    10个美妙的Python装饰器
  • 原文地址:https://blog.csdn.net/m0_52192682/article/details/126036784