iOS详解多线程(实现篇——GCD)

多线程-GCD.png

上一节中,我们学习了苹果官方提供的面向对象的实现多线程的方法——NSThread。这一节中,我们学习C语言的实现多线程的方法,GCD,这也是我们项目中经常使用的一种方法。
NSThread链接:详解多线程(实现篇——NSThread)
多线程概念篇链接:详解多线程(概念篇——进程、线程以及多线程原理)

源码链接:https://github.com/weiman152/Multithreading.git

多线程的实现方法

1.NSThread(OC)

2.GCD(C语言)

3.NSOperation(OC)
4.C语言的pthread(C语言)
5.其他实现多线程方法

本章主要内容提示:

  1. GCD概念
  2. 重要概念:任务(同步、异步)
  3. 重要概念:队列(串行、并发)
  4. GCD的使用
  5. 案例:图片下载
  6. 栅栏函数
  7. 延迟执行
  8. 一次性代码
    8_1. 一次性代码案例:单例
  9. 快速迭代
  10. 队列组
    10_1. 队列组案例(下载图片之后合成图片)
1.GCD概念

GCD的全程为Grand Central Dispatch,简单翻译为大中央调度。也是苹果官方开发的解决多线程的一种方式。
GCD是C语言的,充分利用CPU的多核的并行运算的一种解决多线程的方法。

在GCD中,有两个重要的概念:任务和队列。我们在使用GCD的时候,都是在与这两个概念打交道,如果这两个概念不清楚,我们使用的时候就会混淆。下面呢,我们就一起学习这两个重要的概念吧。

2. 重要概念:任务(同步、异步)

什么是任务?
我们上学的时候,老师说,给你布置一项任务吧,今天负责收取全班同学的作业。收取作业这个操作就是任务。所以,任务是什么?就是要执行的操作。很好理解吧?
什么是同步(sync)?
不开启新的线程,把任务添加到当前线程中,在任务完成之前一直等待。
什么是异步(async)?
可以开启新的线程,任务可以添加到新的线程中,不等待队列完成,可以继续执行。
举个例子🌰:
假如你要做午饭这个任务。
同步执行就是你只有一个锅,一个炉子,只能先把米饭做熟,然后再去炒菜。
异步执行就是你有两个锅,两个炉子,一个锅里做米饭,一个锅里炒菜。

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

3. 重要概念:队列(串行、并发)

队列(Dispatch Queue)是什么?
队列也很好理解的。我们都见过排队,排队买饭、排队取票、排队上车等等。排队的时候,总是队伍前面的先进入,后面的后进入,这就是先进先出(FIFO)。
队列的示意图如下:


队列示意图.png

GCD中的队列,就是对多个线程进行管理的。
在队列中,还有两个概念,串行队列和并发队列。
什么是串行队列?
我们都吃过糖葫芦,一串一串的,我们也吃过烤串,也是一串一串的,总是先吃上面的在吃下面的,一个一个的吃。串行队列也是类似的,一个任务完成了,再进行下一个任务。


串行队列.png

什么是并发队列?
可以开启多个线程,让任务并发执行。但是,并发队列只有在异步任务的时候才会有效。


并发+异步.png

弄明白了这些概念,我们就开始使用GCD创建多线程吧。

4. GCD的使用

GCD的使用步骤:
1》创建队列(串行、并发);
2》定制任务(同步、异步)(想要做的事);
3》将任务添加到队列中等待执行。
GCD会自动将队列中的任务取出来,放到对应的线程中执行。任务取出原则还是先进先出。

创建队列

//1. 创建队列
/**
// 第一个参数const char *label : C语言字符串,用来标识
// 第二个参数dispatch_queue_attr_t attr : 队列的类型
// 并发队列:DISPATCH_QUEUE_CONCURRENT
// 串行队列:DISPATCH_QUEUE_SERIAL 或者 NULL
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
*/

//队列
-(void)test{

    //1.1 并发队列
    //可以开启多个线程,任务并发执行
    dispatch_queue_t BFqueue = dispatch_queue_create("BFqueue", DISPATCH_QUEUE_CONCURRENT);
    //1.2 串行队列
    //任务一个接一个的执行,在一个线程中
    dispatch_queue_t CXqueue = dispatch_queue_create("CXqueue", DISPATCH_QUEUE_SERIAL);
    //1.3 系统默认提供全局并发队列,供使用
    /**
    系统默认全局队列 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     第一个参数:队列优先级
     #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
     #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认
     #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
     #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
     第二个参数: 预留参数  0
     */
    dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //1.4 获得主队列
    dispatch_queue_t mainQ = dispatch_get_main_queue();
    
}
创建任务
//任务
-(void)test2{
   //1. 同步任务:立刻开始执行
    /**
     第一个参数:队列
     第二个参数:要执行的操作,是个Block
     */
    dispatch_sync(dispatch_get_main_queue(), ^{
        //同步执行的任务
    });
    
    //2. 异步任务:等主线程执行完以后,开启子线程执行任务
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //异步执行的任务
    });
}
任务和队列的组合

下面我们把任何和队列进行排列组合,看看每一种的结果。
1.同步任务+串行队列
2.同步任务+并发队列
3.异步任务+串行队列
4.异步任务+并发队列
5.同步任务+主队列
6.异步任务+主队列
7.同步任务+全局队列
8.异步任务+全局队列

1.同步任务+串行队列
//1.同步任务+串行队列
- (IBAction)gcdTest1:(id)sender {
    //当前线程在主线程
   [self test1_1];
    /**
    总结:同步任务是不开启新的线程,把任务添加到当前线程中,当前线程是主线程,所以是添加到主线程中.串行队列是任务一个接一个的执行。这里有三个任务,也是先执行任务一,然后执行任务二,最后是任务三。
     打印结果:
     2020-09-29 15:03:58.965539+0800 多线程[3069:121374] 开始测试啦!
     2020-09-29 15:03:58.965869+0800 多线程[3069:121374] 1.同步任务+串行队列
     2020-09-29 15:03:58.966169+0800 多线程[3069:121374] 当前线程:<NSThread: 0x600000478980>{number = 1, name = main}
     2020-09-29 15:04:00.967371+0800 多线程[3069:121374] 同步任务二+串行,睡了2秒
     2020-09-29 15:04:00.967774+0800 多线程[3069:121374] 当前线程:<NSThread: 0x600000478980>{number = 1, name = main}
     2020-09-29 15:04:00.967987+0800 多线程[3069:121374] 同步任务三+串行
     2020-09-29 15:04:00.968206+0800 多线程[3069:121374] 当前线程:<NSThread: 0x600000478980>{number = 1, name = main}
     2020-09-29 15:04:00.968428+0800 多线程[3069:121374] 测试结束啦!
     */
    
    //我们自己创建个线程试试看
    [NSThread detachNewThreadWithBlock:^{
        [self test1_1];
    }];
    //结果与上次一样
    /**
     2020-09-29 15:30:53.381501+0800 多线程[3253:131992] 开始测试啦!
     2020-09-29 15:30:53.381831+0800 多线程[3253:131992] 1.同步任务+串行队列
     2020-09-29 15:30:53.382813+0800 多线程[3253:131992] 当前线程:<NSThread: 0x600003f53680>{number = 7, name = (null)}
     2020-09-29 15:30:55.386012+0800 多线程[3253:131992] 同步任务二+串行,睡了2秒
     2020-09-29 15:30:55.386528+0800 多线程[3253:131992] 当前线程:<NSThread: 0x600003f53680>{number = 7, name = (null)}
     2020-09-29 15:30:55.386772+0800 多线程[3253:131992] 同步任务三+串行
     2020-09-29 15:30:55.387163+0800 多线程[3253:131992] 当前线程:<NSThread: 0x600003f53680>{number = 7, name = (null)}
     2020-09-29 15:30:55.387616+0800 多线程[3253:131992] 测试结束啦!
     */
}

-(void)test1_1{
    NSLog(@"开始测试啦!");
    //1.同步任务+串行队列
    dispatch_queue_t cxQ1 = dispatch_queue_create("串行队列一", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(cxQ1, ^{
        NSLog(@"1.同步任务+串行队列");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(cxQ1, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"同步任务二+串行,睡了2秒");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(cxQ1, ^{
        NSLog(@"同步任务三+串行");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束啦!");
}

示意图:


同步任务+串行队列.png
2.同步任务+并发队列
//2.同步任务+并发队列
- (IBAction)gcdTest2:(id)sender {
    NSLog(@"开始测试啦,2.同步任务+并发队列");
    dispatch_queue_t bfQ = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(bfQ, ^{
        NSLog(@"任务一");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(bfQ, ^{
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"任务二");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(bfQ, ^{
        NSLog(@"任务三");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束啦");
    /**
     总结:同步任务,把任务添加到当前线程,当前在主线程,所以三个任务都添加到主线程中执行。
     */
}

打印结果:


image.png

总结:同步任务,把任务添加到当前线程,当前在主线程,所以三个任务都添加到主线程中执行。同步任务不具备开启新线程的能力,所以没有新的线程。

示意图如下:


image.png
3.异步任务+串行队列
//3.异步任务+串行队列
- (IBAction)gcdTest3:(id)sender {
    NSLog(@"开始测试,3.异步任务+串行队列");
    dispatch_queue_t cxq = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    dispatch_async(cxq, ^{
        NSLog(@"任务一");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(cxq, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"任务二");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(cxq, ^{
        NSLog(@"任务三");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束啦!");
}

打印结果:


image.png

总结:
异步任务可以开启新的线程,也不会阻塞当前线程。当前是主线程,因为开启新的线程需要时间,所以先打印的主线程的两句话。开启线程以后,因为是串行队列,任务在队列中一个接一个的执行。
示意图如下:


image.png
4.异步任务+并发队列
//4.异步任务+并发队列
- (IBAction)gcdTest4:(id)sender {
    NSLog(@"测试开始,异步任务+并发队列");
    dispatch_queue_t bfq = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(bfq, ^{
        NSLog(@"任务一");
        NSLog(@"1.当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(bfq, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"任务二");
        NSLog(@"2.当前线程:%@",[NSThread currentThread]);
    });
    dispatch_async(bfq, ^{
        NSLog(@"任务三");
        NSLog(@"3.当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束了!");
}

打印结果:


image.png

总结:
异步任务具备开启新的线程的能力,此案例中有三个任务,开启了三个线程。并发队列把任务分配给子线程中执行。

示意图如下:


image.png
5.同步任务+主队列(死锁🔐)
//5.同步任务+主队列:死锁
- (IBAction)gcdTest5:(id)sender {
    NSLog(@"测试开始,同步任务+主队列");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任务一");
        NSLog(@"1.线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束");
}

结果:


死锁.png

为什么嘞?
我们把代码按照任务详细分一下:


image.png

网上很多人会把上面代码分为三个任务,但是我认为,分为四个任务更能够理解死锁是如何产生的,因为即使任务四不存在,依然会发生死锁。

个人分析:
当前在主线程中,任务一顺利执行,然后执行dispatch_sync这个函数,也就是任务二,函数体要执行的是任务三。任务二要等任务三执行完成才能返回。因为主线程是串行的,所以任务三是在任务二后面执行的。这就造成了任务二等待任务三执行完返回,任务三要等待任务二返回才能执行。它俩就这样僵持住了,互相等待了,造成了死锁。
示意图如下:


image.png
6.异步任务+主队列
//6.异步任务+主队列
- (IBAction)gcdTest6:(id)sender {
    NSLog(@"测试开始,异步任务+主队列");
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"任务一");
        NSLog(@"1.线程:%@",[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"任务二");
        NSLog(@"2.线程:%@",[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"任务三");
        NSLog(@"3.线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束。");
}

打印结果:


image.png

总结:
我们知道,主队列是串行队列,异步任务可以开启新的线程,但是不一定会开启新的线程。这里就没有开启新的线程,而是把任务添加到主线程中。异步任务不会阻塞主线程,所以先打印了主线程中的两句话,然后顺序执行三个任务。
示意图如下:


image.png
7.同步任务+全局队列(也是并发队列)
//7.同步任务+全局队列(也是并发队列)
- (IBAction)gcdTest7:(id)sender {
    NSLog(@"开始,同步任务+全局队列(也是并发队列)");
    dispatch_queue_t global = dispatch_get_global_queue(0, 0);
    dispatch_sync(global, ^{
        NSLog(@"任务一");
        NSLog(@"1.线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(global, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"任务二");
        NSLog(@"2.线程:%@",[NSThread currentThread]);
    });
    dispatch_sync(global, ^{
        NSLog(@"任务三");
        NSLog(@"3.线程:%@",[NSThread currentThread]);
    });
    NSLog(@"测试结束了!");
}

结果:


image.png

总结:
同步任务不开启新的线程,而是把任务添加到当前线程,也就是主线程中。全局队列是个并发队列,并发队列只有在异步任务的时候才能起作用,因为当前只有一个线程,并发是无效果的。因为同步任务会阻塞当前线程,直到任务完成。所以会先打印最上面的一行在主线程中的代码,接着阻塞,执行同步任务,也就是任务一、任务二和任务三,直到所有任务执行完了,在继续执行主线程的最后一行打印。

示意图如下:


image.png
8.异步任务+全局队列(也是并发队列)
//8.异步任务+全局队列(也是并发队列)
- (IBAction)gcdTest8:(id)sender {
    NSLog(@"开始,异步任务+全局队列");
    dispatch_queue_t global = dispatch_get_global_queue(0, 0);
    dispatch_async(global, ^{
        NSLog(@"任务一");
        NSLog(@"1.线程:%@",[NSThread currentThread]);
    });
    dispatch_async(global, ^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"任务二");
        NSLog(@"2.线程:%@",[NSThread currentThread]);
    });
    dispatch_async(global, ^{
        NSLog(@"任务三");
        NSLog(@"3.线程:%@",[NSThread currentThread]);
    });
    
    NSLog(@"测试结束了");
}

打印结果:


image.png

总结:
异步任务是可以开启新线程的。这里的三个异步任务开启了三个线程。全局队列也是并发队列,把任务分发给三个子线程中执行。主线程不会阻塞,所以直接打印了主线程的内容。开启子线程需要时间。由于任务二睡了2秒,所以先打印的任务一和任务三,最后是任务二。
示意图如下:


image.png

GCD的任务和队列的总结:


image.png

简单一点:


image.png

观察之后发现,
同步任务都不开启新的线程,都会阻塞当前线程,大部分都是串行执行任务。
异步任务都不会阻塞当前线程,大部分会开启新的线程。

5. 案例:图片下载

因为下载图片是耗时操作,一般我们都会放在子线程中进行下载,等图片下载完成以后,再把图片放在主线程中显示。
下面我们就用GCD演示这一过程。

//案例:下载图片,并显示
- (IBAction)downLoad:(id)sender {
    NSString * url = @"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1601469152745&di=b68e17d74c30d400e1df9473ded0eb1c&imgtype=0&src=http%3A%2F%2Fhbimg.huabanimg.com%2F959c8754d77244788f5a8f775ee36dec4a5d362e236d9-eP3CI9_fw658";
    dispatch_queue_t global = dispatch_get_global_queue(0, 0);
    dispatch_async(global, ^{
        NSURL * imgUrl = [NSURL URLWithString:url];
        NSData * data = [NSData dataWithContentsOfURL:imgUrl];
        UIImage *image = [UIImage imageWithData:data];
        NSLog(@"下载图的线程:%@",[NSThread currentThread]);
        //回到主线程,给图片赋值
        dispatch_async(dispatch_get_main_queue(), ^{
            self.showImg.image = image;
            NSLog(@"显示图的线程:%@",[NSThread currentThread]);
        });
    });
}

结果:


待下载.png
下载完.png

打印结果:


image.png
6. 栅栏函数

dispatch_barrier_async(queue, ^{
NSLog(@"###############我是个栅栏################");
});

栅栏函数的作用是,控制任务的执行顺序.
我们用代码试试看,我们使用异步任务+并发队列。

//栅栏函数
- (IBAction)zhalan:(id)sender {
    //栅栏函数,可以控制任务的执行顺序
    //1.创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    //2.创建任务,异步任务
    dispatch_async(queue, ^{
        NSLog(@"任务一");
        [self run:@"1"];
    });
    dispatch_async(queue, ^{
        NSLog(@"任务二");
        [self run:@"2"];
    });
    dispatch_async(queue, ^{
        NSLog(@"任务三");
        [self run:@"3"];
    });
    dispatch_async(queue, ^{
        NSLog(@"任务四");
        [self run:@"4"];
    });
    
}

-(void)run:(NSString *)mark {
    for (int i = 0; i<5; i++) {
        NSLog(@"任务:%@,i = %d, 线程:%@",mark,i,[NSThread currentThread]);
    }
}

打印结果:


image.png

并发执行的线程,任务的顺序是不可控的。

我们使用栅栏函数后看看。

//栅栏函数
- (IBAction)zhalan:(id)sender {
    //栅栏函数,可以控制任务的执行顺序
    //1.创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    //2.创建任务,异步任务
    dispatch_async(queue, ^{
        NSLog(@"任务一");
        [self run:@"1"];
    });
    dispatch_async(queue, ^{
        NSLog(@"任务二");
        [self run:@"2"];
    });
    //栅栏函数
    dispatch_barrier_async(queue, ^{
        NSLog(@"###############我是个栅栏################");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任务三");
        [self run:@"3"];
    });
    dispatch_async(queue, ^{
        NSLog(@"任务四");
        [self run:@"4"];
    });
    
}

-(void)run:(NSString *)mark {
    for (int i = 0; i<5; i++) {
        NSLog(@"任务:%@,i = %d, 线程:%@",mark,i,[NSThread currentThread]);
    }
}
image.png

image.png

我们发现,使用栅栏函数之后,栅栏前面的两个任务先执行完,然后执行栅栏函数,最后在执行后面的任务。就像一座栅栏一样,把异步执行的无顺序的任务隔离开了,变成了部分有序的代码。

7. 延迟执行

有的时候,我们需要延迟几秒后在执行某些操作,这个时候,我们就可以使用GCD的dispatch_after来实现啦。
dispatch_after不会阻塞当前线程,可以在主线程,也可以在子线程中执行代码。

//延迟执行
- (IBAction)yanchi:(id)sender {
    //在主线程中延迟2秒执行
    NSLog(@"开始啦!");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"哈哈哈哈哈,延迟了嘛");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"结束啦");
    
    //子线程中延迟3秒执行
    NSLog(@"再次开始啦");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"哎呀呀,3秒哦");
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
    NSLog(@"又一次结束啦!");
}

结果:


image.png

延迟执行,我们还有其他的方法,比如:

//延迟执行的其他方法
    [self performSelector:@selector(YC) withObject:nil afterDelay:2.0];
    
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(YC) userInfo:nil repeats:NO];

-(void)YC {
    NSLog(@"延迟执行哟,线程:%@",[NSThread currentThread]);
}

结果:


image.png

也是可以延迟执行的。但是呢,这两个方法都是在主线程中执行的。

8. 一次性代码

dispatch_once
我们在创建单例的时候经常使用GCD的dispatch_once这个方法,保证代码只会执行一次。

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"此代码只会执行一次。");
    });

因为此段代码常用于单例中,我们创建个单例试试。

8_1. 一次性代码案例:单例
image.png

代码如下:
GCDTest.h

//
//  GCDTest.h
//  Multithreading
//
//  Created by wenhuanhuan on 2020/10/2.
//  Copyright © 2020 weiman. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface GCDTest : NSObject<NSCopying,NSMutableCopying>

+(instancetype)shareGCDTest;

@end

NS_ASSUME_NONNULL_END

GCDTest.m

//
//  GCDTest.m
//  Multithreading
//
//  Created by wenhuanhuan on 2020/10/2.
//  Copyright © 2020 weiman. All rights reserved.
//

#import "GCDTest.h"

@implementation GCDTest

+(instancetype)shareGCDTest{
    return [[self alloc] init];
}

static GCDTest * instance;
+(instancetype)allocWithZone:(struct _NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (instance==nil) {
            instance = [super allocWithZone:zone];
        }
    });
    return instance;
}

-(id)copyWithZone:(NSZone *)zone{
    return instance;
}

-(id)mutableCopyWithZone:(NSZone *)zone{
    return instance;
}
@end

测试:

//一次性代码
- (IBAction)onceAction:(id)sender {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"此代码只会执行一次。");
    });
    
    //创建个单例试试
    GCDTest * test1 = [[GCDTest alloc] init];
    GCDTest * test2 = [GCDTest shareGCDTest];
    GCDTest * test3 = [test1 copy];
    GCDTest * test4 = [test1 mutableCopy];
    NSLog(@"test1:%@",test1);
    NSLog(@"test2:%@",test2);
    NSLog(@"test3:%@",test3);
    NSLog(@"test4:%@",test4);
}

看看结果:


image.png

我们发现,我们创建的四个对象的地址都是一样的,这就是一个合理的单例啦。

9. 快速迭代

我们需要使用循环的时候,一般用for循环,for_in循环,while循环,do...while循环,swift中还有更多其他的循环。在GCD中,也给我们提供了一种快速的循环方法,dispatch_apply。

/*
第一个参数:迭代的次数
第二个参数:在哪个队列中执行
第三个参数:block要执行的任务
*/
dispatch_apply(10, queue, ^(size_t index) {
});

为什么它是快速迭代呢?
因为它会开启多个线程并发执行循环体内的操作。
如果在串行队列中,dispatch_apply就和for循环一样顺序执行,就没有意义了。
在并发队列中,dispatch_apply会开启多个线程并发执行,提高效率。

//快速迭代
- (IBAction)kuaisu:(id)sender {
    NSLog(@"开始快速迭代");
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"index=%zd, 线程:%@",index, [NSThread currentThread]);
    });
    
}

结果:


image.png
10. 队列组

队列组可以在组中的并发线程都执行完成后,进行某些操作。

// 创建队列组
    dispatch_group_t group = dispatch_group_create();
    // 创建并行队列 
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 执行队列组任务
    dispatch_group_async(group, queue, ^{   
    });
    //队列组中的任务执行完毕之后,执行该函数
    dispatch_group_notify(group, queue, ^{
    });

看个简单例子:

//队列组
- (IBAction)duilie:(id)sender {
    //创建队列组
    dispatch_group_t group = dispatch_group_create();
    //创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("并发", DISPATCH_QUEUE_CONCURRENT);
    //执行队列组任务
    dispatch_group_async(group, queue, ^{
        NSLog(@"这是个队列组中的任务,编号1");
        NSLog(@"任务一:线程%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"这是个队列组中的任务,编号2,睡了1秒");
        NSLog(@"任务二:线程%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"这是个队列组中的任务,编号3");
        NSLog(@"任务三:线程%@",[NSThread currentThread]);
    });
    //队列组执行完之后执行的函数
    dispatch_group_notify(group, queue, ^{
        NSLog(@"队列组任务执行完成。");
    });
}

看看打印结果:


image.png

我们在项目中,也是会经常遇到这样的问题的。比如,我们要显示一个页面,但是这个页面有三个接口,我们需要等待三个接口的数据都返回来之后再进行显示。这个时候,就可以把三个网络请求放在队列组中,等待全部完成后,在进行UI显示。

这里,我们举一个例子吧。
我们要进行图片合成的操作,需要下载两张图片,等两张图片都下载完成了,再把图片合成一张。

10_1. 队列组案例(下载图片之后合成图片)

UI如下:


image.png

代码如下:

@property (weak, nonatomic) IBOutlet UIImageView *imageOne;
@property (weak, nonatomic) IBOutlet UIImageView *imageTwo;
@property (weak, nonatomic) IBOutlet UIImageView *finalImage;
@property (nonatomic,strong)UIImage * image1;
@property (nonatomic,strong)UIImage * image2;
//队列组案例:合成图片
- (IBAction)groupDemo:(id)sender {
    //1.创建队列组
    dispatch_group_t group = dispatch_group_create();
    //2.创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("并发", DISPATCH_QUEUE_CONCURRENT);
    //3.下载图片一
    dispatch_group_async(group, queue, ^{
        NSString * str = @"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1601638749466&di=92ace2ffa924fe6063e7a221729006b1&imgtype=0&src=http%3A%2F%2Fpic.autov.com.cn%2Fimages%2Fcms%2F20119%2F6%2F1315280805177.jpg";
        self.image1 = [self loadImage:str];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageOne.image = self.image1;
        });
        
    });
    //4.下载图片二
    dispatch_group_async(group, queue, ^{
        NSString * str = @"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1601638873771&di=07129fd95c56096a4282d3b072594491&imgtype=0&src=http%3A%2F%2Fimg.51miz.com%2Fpreview%2Felement%2F00%2F01%2F12%2F49%2FE-1124994-5FFE5AC7.jpg";
        self.image2 = [self loadImage:str];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageTwo.image = self.image2;
        });
    });
    //5.合成图片
    dispatch_group_notify(group, queue, ^{
        //图形上下文开启
        UIGraphicsBeginImageContext(CGSizeMake(300, 200));
        
        //图形二
        [self.image2 drawInRect:CGRectMake(0, 0, 300, 200)];
        //图形一
        [self.image1 drawInRect:CGRectMake(100, 50, 100, 100)];
        //获取新的图片
        UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
        //关闭上下文
        UIGraphicsEndImageContext();
        //回到主线程,显示图片
        dispatch_async(dispatch_get_main_queue(), ^{
            self.finalImage.image = image;
            NSLog(@"完成图片的合成");
        });
    });
}
//下载图片
-(UIImage *)loadImage:(NSString *)strUrl {
    NSLog(@"当前线程:%@",[NSThread currentThread]);
    NSURL * url = [NSURL URLWithString:strUrl];
    NSData * data = [NSData dataWithContentsOfURL:url];
    UIImage * image = [UIImage imageWithData:data];
    return image;
}

结果如下图:


image.png

注意图片的顺序。

到此为止,GCD的基本内容我们已经学习完了,如有遗漏错失,还请留言指教,谢谢!
下一节中,我们将探究NSOperation实现多线程。
祝大家生活愉快!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,639评论 1 302
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,591评论 1 258
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,050评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,169评论 0 180
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,976评论 1 258
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,876评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,476评论 2 273
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,229评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,095评论 6 234
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,579评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,356评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,698评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,272评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,168评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,605评论 3 210
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,645评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,035评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,609评论 2 232
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,698评论 2 233