[iOS开发]GCD

385 阅读26分钟

GCD简介

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

我们为什么要使用GCD呢

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

GCD任务和队列

任务

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

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

一个简单的例子:我们需要打电话给A和B

同步执行:打电话给A的时候,不能同时打给B。只有等到A打完了,才能打给B。(等待任务结束)而且只能用当前电话(不具备开启新线程的能力)

异步执行:打电话给A的时候,不用等着和A通话结束(不用等待任务执行结束),还能同时给B打电话。而且除了当前电话,我们还可以使用其他一个或多个电话(具备开启其他线程的能力)

注意是异步执行虽然具有开启新线程的能力,但是并不一定开启新线程。

队列

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

队列也是主要分为两种==串行队列==和==并发队列== 两者都符合FIFO的原则。两者主要的区别是:==执行顺序不同 ,以及开启线程数不同==

  • 串行队列
    • 每次只有一个任务被执行。让任务一个接一个的执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 并发线程
    • 可以让多个任务并发(同时执行)。(可以开启多个线程,并且同时执行任务)
    • 并发队列的并发功能只有在异步方法下才有效

GCD使用步骤

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

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

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

  • 第一个参数用于表示队列的唯一标识符,用于DEBUG,可以为空。但是还是推荐使用应用程序ID这种逆序全程域名。
  • 第二个参数用来识别是串行队列还是并发队列。
    • DISPATCH_QUEUE_SERIAL或指定为NULL,表示串行队列
    • DISPATCH_QUEUE_CONCURRENT指定为并发队列

对于串行队列,GCD提供了一种特殊的串行队列:主队列(Main Dispatch Queue)

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

对于并发队列,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);

任务的创建方法

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

dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
});

虽然创建任务只需两步,但是既然我们有两种队列(串行队列/并发队列),两种任务执行方式(同步执行/异步执行) 所以总共有四个组合

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

实际上还有两种特殊队列:全局并发队列、主队列。 全局并发队列可以作为普通的并发队列 主队列就不一样了

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

不同的组合有什么区别?

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

先来考虑最简单的使用情况 ==主线程==下 ==不同队列==+==不同任务== 组合使用的不同区别 暂时不考虑==队列嵌套队列==这种复杂情况

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

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

从表格可以看出 ==主线程==中调用==主队列==+==同步执行==会导致死锁的问题 在这里插入图片描述

这是因为 ==主队列中追加的同步任务==和==主线程本身的任务==两者之间相互等待,阻塞了==主队列==,最终造成了主队列的死锁

我们在其他线程调用主队列+同步执行,则不会阻塞主队列,自然不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务

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

除了上面在【主线程】中调用【主队列】+【同步执行】会导致死锁问题。实际在使用【串行队列】的时候,也可能出现阻塞【串行队列】所在线程的情况。比如同一个串行队列的嵌套使用。

在【异步执行】 + 【串行队列】的任务中,又嵌套了【当前的串行队列】,然后进行【同步执行】 在这里插入图片描述 执行这个代码会导致==串行队列中追加的任务==和==串行队列中原有的任务==两者之间相互等待,阻塞了【串行队列】,造成了串行队列所在线程(子线程)死锁问题

不同队列+不同任务的组合,以及【队列中嵌套队列】的区别 后面细讲 这里先扔个结论 在这里插入图片描述

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

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

我们以这个为例子,人好比是任务,管理员好比是系统,入口则代表线程。

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

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

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

所以换成GCD来说

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

GCD的基本使用

开干!

同步+并发

特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务 相当于开了一个门,虽然有多只队伍,但是一个如果一次只能通过一个人,所以只好一个一个的走过去

我们新建三个同步执行的任务 每个都有模拟2秒的耗时 并且打印其所在线程

- (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");
}

结果如下 在这里插入图片描述

  • 我们可以看到所有的任务都是在当前主线中执行的,并没有开启新线程。(同步执行没有开启新线程的能力)
  • 所有任务都在begin---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");
}

在这里插入图片描述

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

同步+串行

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

- (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");
}

在这里插入图片描述 结果和同步+并发一样,我们很清楚的可以明白,虽然上面那个叫并发,但是同步把其进行了限制,结果和同步串行一样

异步+串行

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

- (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");
}

在这里插入图片描述

  • 开启了一条新线程(==异步具备开启新线程的能力,串行仅仅开启了一个线程== 相当于异步强行搞出来了新线程,但是串行保证其仅仅就是一个新线程,这是和并行不一样的)

同步+主队列

相互卡住不可执行

- (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");
}

直接crash了 在这里插入图片描述

这是因为我们在主线程中直接执行同步+主队列这个函数 相当于把这个函数放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕才会接着执行。当我们把【任务1】追加到主队列中,【任务1】就在等待主线程处理完syncMain任务。而syncMain任务需要等待【任务1】执行完毕,才能接着执行。

syncMain和【任务1】都在等待对方执行完毕。大家相互等待,造成死锁 并且syncMain---end也没有被打印

在其他线程中调用同步+主队列

// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];

在这里插入图片描述

没有开启新线程,所有任务是按顺序执行

那么为什么在其他线程调用就是正确的呢,不会产生死锁呢

因为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");
}

在这里插入图片描述

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

GCD其他方法

栅栏方法:dispatch_barrier_async

  • 我们有时候需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,当然操作组里也可以包含一个或者多个任务。
  • 这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏
  • dispatch_barrier_async方法会等待前边追加到并发队列中的任务全部执行完毕后,再将制定的任务追加到该异步队列中。然后在dispatch_barrier_async方法追加的任务执行完毕之后,接着追加任务到该异步队列并开始执行
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]);      // 打印当前线程
    });

在这里插入图片描述

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

延时执行方法:dispatch_after

在Block的强弱共舞的例子有学过

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

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
// 在3秒后追加Block到Main Dispatch Queue中
dispatch_after(time, dispatch_get_main_queue(), ^{
    
    NSLog(@"waited at least three seconds");;
});

dispatch_after函数
第一个参数:
指定时间的dispatch_time_t类型的值,可以使用dispatch_time函数或dispatch_walltime函数作成。

第二个参数:
要追加处理的Dispatch Queue。

第三个参数:
要执行处理的Block。

dispatch_time这个函数
第一个参数:
指定时间开始
第二个参数:
指定时间后的时间,通常用于计算相对时间。DISPATCH_TIME_NOW 表示现在的时间。

“ull”是C语言的数值字面量,是显示表明类型时使用的字符串。
NSEC_PER_SEC单位:毫微秒
NSEC_PER_MSEC单位:毫秒

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

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

static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行 1 次的代码(这里面默认是线程安全的)
    });

快速迭代方法:dispatch_apply

我们通常是使用for循环遍历的方法,但是GCD给我们提供了快速迭代的方法dispatch_apply

其按照指定的此处将指定的任务追加到指定的队列中,并等待全部队列执行结束。

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

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

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

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");
/*
第一个参数表示重复次数
第二个参数表示将要执行处理的queue
第三个参数:要执行的处理,带有参数,第多少次,是为了按第一个参数重复追加Block并区分各个Block而使用的。
*/

在这里插入图片描述

这里不需要考虑同步不同步的问题 其仅仅就是遍历队列

因为是在并发队列中异步执行任务 所以各个任务的执行时间长短不定,最后的结束顺序也不能确定,但是一定在begin---end中,因为dispatch_apply方法会等待全部任务执行完毕

队列组:dispatch_group

分别异步执行两个耗时任务,然后当两个耗时任务都执行完毕后再回到主线程执行任务。这就是我们用到队列组的情况

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

dispatch_group_notify

监听group中任务的完成状态,当所有的任务都执行完成后,追加任务到group中,并执行任务

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
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程

        NSLog(@"group---end");
    });
  • dispatch_group_create函数生成dispatch_group_t类型的group
  • dispatch_group_async函数,第一个参数为生成的group,第二个参数为队列,第三个参数是Block
  • dispatch_group_notify函数第一个参数为需要监视的group,第二个参数为要追加结束处理的队列,第三个结束对于我们追加的队列进行Block的处理 在这里插入图片描述 当所有任务都执行完成之后,才执行notify相关block中的任务

dispatch_group_wait

暂停当前线程,等待指定的group中的任务完成后,才会继续往下执行

- (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
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"group---end");
    NSLog(@"currentThread---%@",[NSThread currentThread]);

}
  • dispatch_group_wait函数的第一个参数为等待的Dispatch Group,第二个参数为等待的时间,就是dispatch_time_t类型 在这里插入图片描述 wait会暂停现在的线程,去执行wait中的线程,wait中的线程执行结束后,才会执行其之后的操作。所以使用dispatch_group_wait会造成当前线程的阻塞

在这里插入图片描述

dispatch_group_enter、dispatch_group_leave

这个相当于我们使用dispatch_group_enter、dispatch_group_leave来实现了dispatch_group_async的功能

  • dispatch_group_enter标志着一个任务追加到了group,执行一次,相当于group中未执行完毕任务数+1。
  • dispatch_group_leave标志着一个任务离开了group,执行一次,相当于group中未执行完毕任务数-1。
  • 当group中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除对当下线程的阻塞,以及执行追加到notify中的任务
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
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程

        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程.
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    
        NSLog(@"group---end");
    });

在这里插入图片描述 其实就是等同于使用dispatch_group_async

里面只有一个参数就是group dispatch_group_enter(group); dispatch_group_leave(group);

信号量:dispatch_semaphore

  • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_signal: 发送signal信号,信号量+1
  • dispatch_semaphore_wait:使信号量-1
  • (原来在iOS也逃不过PV操作😢)信号量的计数值小于0时等待 (阻塞),大于等于0时正常执行。(官方注释:如果结果值小于零,此函数在返回前等待信号出现。)所以和操作系统学习的信号量的判读还不太一样 在这里插入图片描述

dispatch Semaphore在实际开发中主要用于

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

dispatch 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);
}

在这里插入图片描述 可以看到,是在semaphore---end是在number = 100之后才打印的。而且输出结果number为100。还是那句话【异步执行】不会做任何等待,可以继续执行任务。

  • semaphore初始创建时引用计数为0
  • 异步执行将任务1追加到队列之后,不做等待,接着执行dispatch_semaphore_wait方法,semaphore减一,此时semaphore == -1,当前线程进入等待状态
  • 然后,异步任务1开始执行。任务1执行到duspatch_semaphore_signal之后,信号量加了个1,semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行
  • 最后打印end, number == 100

我们如果不使用信号量来控制同步,异步执行不会做任何等待,继续创建任务 在这里插入图片描述 block依旧保持0

如此便实现了线程同步,将异步转成同步的过程(函数中的BLock变量仅仅是用来判断执行先后顺序的变量)

线程安全与线程同步(为线程+锁)

我们在atomic和nonatomic中已经清楚的知道什么是线程的不安全

不考虑线程安全(不使用Semaphore)

- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 20;
    //假设一共有20张火车票,1和2表示两个窗口
    // queue1 代表售卖窗口1
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表售卖窗口2
    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 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"已售完");
            break;
        }
        
    }
}

在分析这个代码之前,我们回到最开始 毫无疑问 串行+异步 不过与上面不同的是,这里创建了两个队列 所以串行+异步的结果和并行+异步的结果一样,创建了两个currentThread都是新线程

同理 两个队列在同步的条件下 还是没有开启新线程,串行的执行任务

回到上面这个代码 我们没有考虑线程安全的问题

此时的结果 在这里插入图片描述 票数是混乱的 并不能确定两个线程在买票时是否与另一个线程相绑定

加锁来确保线程安全

还是上面那个例子

- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);
    
    self.ticketSurplusCount = 20;
    
    // 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 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }
        
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}

在这里插入图片描述

从结果看很完美 看一下具体的实现思路

  • 我们先把信号量设置为1,假设第一个线程进入sale方法时,调用wait减一,信号量为0,正常执行,不需要阻塞。
  • 在此时,第二个线程又需要进入sale方法,此时第一个线程的操作还没有结束,wait操作从而导致此时信号量小于了0,所以阻塞等待信号量正常才继续执行。
  • 在第一个线程卖完票后,signal是信号量又变为了0,第二个等待的线程现在开始执行。

所以在这里相当于售票是个临界区,我们需要对临界区进行互斥访问,一次仅允许进入一个线程,有点类似于临界区资源打印机的那个过程,我们通过PV操作来避免这个情况

PS: 这个github的demo太舒服了 永远滴神

问题

在这里插入图片描述

之前没有考虑循环创建异步任务这部分的内容

先说为什么会报错

因为其是异步的 我们不能确定什么时候开始执行某个异步线程 所以当两个异步线程刚好同时往数组里添加元素时就会报错

但这个问题也让我学到了 并不是每个分配一个线程,就像for循环十个,不会创建十个线程,在循环中,前面用完的可以给后面用。线程是系统管理的,系统会根据需要创建。

我们在循环中尝试打印一下每个所属的线程 在这里插入图片描述

我们看到确实不是为每个都新创建,而是由系统管控,可能for循环第八次的时候 创建的第一个线程空闲了 就会交给第一个线程去处理 什么是线程空闲了呢 完全由系统进行判断吗 完全随机吗?明天再说吧

  1. 循环嵌套导致的死锁问题未解决---仅仅知道结果 在这里插入图片描述
  2. GCD的任务执行顺序
dispatch_queue_t queue = dispatch_queue_create("qwe", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    NSLog(@"3");
    dispatch_sync(queue, ^{
        NSLog(@"4");
    });
    NSLog(@"5");

在这里插入图片描述 结果是13245

我们来理一下思路

先打印1 接下来将2添加到串行队列上,因为2是异步执行,不会阻塞线程,继续向下执行,打印3。 然后是将4添加到串行队列上,4必须要等2执行后才能继续执行,又因为4是同步任务,会阻塞线程,只有完成4才能继续执行5. 所以1 3 2 4 5

此时线程4在主线程执行,因为同步线程没有开启新线程的能力

此时我们去掉同步的任务 换成NSLog(@"6"); 那么就一个异步执行的任务 我们无法知道其执行的实际和结束的时机,所以每次的打印结果可能都不一样