多线程-GCD

328 阅读28分钟

1. 先来理解几个概念

在接触多线程初期,会接触到诸如 进程、线程、任务、队列、同步、异步、串行、并发等一些字眼,到底应该怎样去解释呢?

进程(Process):进程是操作系统分配资源的最小单位,可以理解为一个正在运行的应用程序。 打开电脑的活动监视器,可以看到我们电脑正在运行的进程有哪些。

截屏2021-03-19 下午4.00.22.png

线程 (Thread):线程是进程处理事务的最小可分割单元。线程被进程用来去执行任务。

进程可以包含多条线程,最少会有一条线程,比如我们的用迅雷下载文件,会有一条线程用来下来文件,同时也会有一个线程在进行一给广告的轮播。

CPU(单核)同一时间只会对某一条线程执行操作,只不过它会在多条线程之前轮换调度,当它调度的足够快的时候,就会造成一种“多线程同时执行”的假象。

现在CPU的核心数越来越多,也就是的系统可以在同一时间内调度多条线程。当然,线程也不是开启的越多越好,如果开启的特别多,CPU需要在很多的线程间调度,导致某条线程内的任务会需要很长的时间间隔才能被轮询,导致CPU资源的浪费,压力过大。另外线程也是需要占用内存够空间的,默认主线程占用1M,子线程占用512KB,线程开启过多会导致内存占用过高。

任务:一条线程需要去下载文件,下载文件的这种操作,就是“任务”,可以理解成“需要做什么是”就是一个“任务”。表现在代码中的可以理解为 GCD 的 block 中的代码,在 Block 语法中记述想要执行的处理并将其追加到 Dispatch Queue中。

任务的执行方式有两种:同步执行和异步执行。

同步执行(sync):任务同步执行时,执行下一个任务的时候,需要等待上一个任务执行完成后,才能执行下个任务。也就是会等dispatch_sync(queue,block) 这个block 完成后才会返回,返回后才能继续执行后面的代码(任务)。在上一个任务没有完成的时候,下一个任务会一直处于任务执行等待的状态。 任务同步执行时只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行(async):与同步执行刚刚相反,执行哪个任务互不影响。任务执行不做任何等待,不用等dispatch_async(queue,block) 这个block执行完成。任务异步执行时,可以在其他新线程中执行,具备开启新线程的能。

同步执行和异步执行的区别在于能不能开启新的线程,具不具备开启新线程的能力和任务的执行是否需要等待。

举个例子:小明早晨起床后,有几个任务分别是:刷牙、刷微博、听歌。

如果同步执行这几个任务的场景是怎样的呢?就是先刷牙,等刷完牙了刷微博,微博刷累了去听歌,如果异步执行这几个任务呢,场景就是起床后刷牙,刷牙时拿起手机刷微博,另外还拿起了另一台手机播放歌曲。

当然举的例子不太严谨,但是帮助理解。

队列(Queue):队列可以理解为一个任务集合,抽象为一个管道中,塞进去了一个一个的任务,这个管道遵循先进先出的规则:新加入的任务放入管道的尾部,需要执行任务是从管道头部取出最近的任务。

截屏2021-03-19 下午5.07.07.png

GCD 中的队列有两种,串行队列(Serial Dispatch Queue)和并行队列(Concurrent Dispatch Queue)两种。

我理解的串行队列和并行队列的区别再去一次性可取出任务数量的区别,串行一次只能取出一个任务,并行一次可以取出多个任务。

串行队列(Serial Dispatch Queue):我理解的串行队列是一次从这一个待处理的任务集合中取出一个任务,然后去执行这个任务。

并行队列(Concurrent Dispatch Queue):我理解的并行队列是一次从这一个任务集合中可以取出多个任务,既然取出了多个任务,就可以在多个线程中同时执行,那么,具体能不能在多个线程中同时执行取决去任务执行的方式,也就是看是同步执行还是异步执行,因为同步执行时,不具备开启新线程的能力,只有一条线程去处理,所以,并发队列 的并发功能只有在异步(dispatch_async)函数下才有效。

2.GCD的执行原理及线程池重用概念

GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用的话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的,线程池是系统自动来维护,不需要我们程序员来维护,而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。

如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程

如果队列中存放的是异步的任务,(注意异步可以开新线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。

这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。

3.GCD的使用方法

GCD的使用可以分为两步:

第一步:创建一个怎样的队列,是串行(Serial)的还是并发(Concurrent)的。

第二步:添加任务到队列中,并指定此任务的执行方式,是同步(sync)执行的还是异步(async)执行的。

3.1 队列的创建

使用dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr)来创建队列,需要传入两个参数。

第一个参数表示队列的唯一标识符,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;

第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列.如果设置为 NULL 将是一个串行队列。

并发队列 的并发功能只有在异步(dispatch_async)函数下才有效

// 创建一个串行队列
dispatch_queue_t queue = dispatch_queue_create("com.xxx.GCD", DISPATCH_QUEUE_SERIAL);
// 创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create("com.xxx.GCD", DISPATCH_QUEUE_CONCURRENT);

系统还预置了两个队列:主队列(dispatch_get_main_queue())全局队列(dispatch_get_global_queue())

// 获取主队列,所有放在主队列中的任务,都会放到主线程中执行
dispatch_queue_t queue = dispatch_get_main_queue();
//获取全局队列(需要传入两个参数,第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT,第二个参数暂时没用,用0即可)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

主队列相当于一个串行队列,全局队列相当于一个并发队列。

由于并发队列的并发功能只有在异步(dispatch_async)函数下才有效,所以全局队列中的任务只有在异步执行时才有意义。

3.2 同/异步执行的指定 并添加任务至队列

在GCD中任务执行的 同步执行(dispatch_sync)异步执行(dispatch_async) 任务方式。

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

4.GCD的具体使用:

我们的队列类型有四种主队列、串行队列、并发队列,全局队列可以看做是一个并发队列,执行任务的方式有两种:同步执行和异步执行,那么在GCD的使用中会有六中组合方式,下面对各个组合来详细使用:

4.1 串行队列任务同步执行

- (void)syncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    NSLog(@"syncSerial---begin");
    dispatch_queue_t syncSerialQueue = dispatch_queue_create("com.app.cp", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(syncSerialQueue, ^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(syncSerialQueue, ^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(syncSerialQueue, ^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncSerial---end");
}

控制台输出:

截屏2021-03-19 下午6.01.23.png

任务的执行是在 begin 和 end 之间,而且任务一个接一个按序执行,并且所有任务的执行都是在当前线程中执行,毕竟任务同步执行不具备开启新线程的能力。

4.2 串行队列任务异步执行

- (void)asyncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncSerial---begin");
    dispatch_queue_t asyncSerial = dispatch_queue_create("com.app.cp", DISPATCH_QUEUE_SERIAL);
    dispatch_async(asyncSerial, ^{
        for (int i = 0; i < 2; ++i) {
           [NSThread sleepForTimeInterval:2];              
            NSLog(@"1---%@",[NSThread currentThread]);      
        }
    });
    dispatch_async(asyncSerial, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"2---%@",[NSThread currentThread]);      
        }
    });
    dispatch_async(asyncSerial, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"3---%@",[NSThread currentThread]);      
        }
    });
    NSLog(@"asyncSerial---end");
}

控制台输出:

截屏2021-03-19 下午6.06.32.png

任务的执行是在 begin 和 end 之后,说明异步任务的执行不会对线程造成阻塞,不去等待,但是由于是串行队列,导致一次只能从任务池中取出一个任务,所以任务会一个接一个按序执行。另外通过打印线程看到已经 开启了1条新的线程 去执行了任务。

4.3 并发队列任务同步执行

- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    NSLog(@"syncConcurrent---begin");
    dispatch_queue_t syncConcurrentQueue = dispatch_queue_create("com.app.cp", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(syncConcurrentQueue, ^{
    // 追加任务1
        for (int i = 0; i<2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(syncConcurrentQueue, ^{
    // 追加任务2
        for (int i = 0; i < 2; ++i) {
            NSLog(@"2---%@",[NSThread currentThread]);      
        }
    });
        
    dispatch_sync(syncConcurrentQueue, ^{
    // 追加任务3
        for (int i = 0; i < 2; ++i) {
            NSLog(@"3---%@",[NSThread currentThread]);      
        }
    });
        
        NSLog(@"syncConcurrent---end");
    
}

控制台输出:

截屏2021-03-19 下午6.13.29.png

所有任务都是在当前队列中执行的,没有开启新的线程,因为同步执行不具备开启新线程的能力.同步任务需要等待队列中的任务执行结束,任务按顺序执行,不能被同时执行。

4.4 并发队列任务异步执行

- (void)asyncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    NSLog(@"asyncConcurrent---begin");
    dispatch_queue_t asyncConcurrent = dispatch_queue_create("com.app.cp", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(asyncConcurrent, ^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(asyncConcurrent, ^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(asyncConcurrent, ^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"asyncConcurrent---end");
}

控制台输出:

截屏2021-03-19 下午6.19.55.png

所有任务都执行在 begin 和 end 之后,而且开启了3个线程,任务的执行时无序的,说明任务之间的执行不做等待。

4.5 主队列任务同步执行

4.5.1 主线程中操作主队列任务同步执行


- (void)syncMainQueue {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  
    NSLog(@"syncMainQueue---begin");
    dispatch_queue_t syncMainQueue = dispatch_get_main_queue();
    //追加任务1
    dispatch_sync(syncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"1---%@",[NSThread currentThread]);      
        }
    });
    dispatch_sync(syncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"2---%@",[NSThread currentThread]);      
        }
    });
    dispatch_sync(syncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"3---%@",[NSThread currentThread]);      
        }
    });
    NSLog(@"syncMainQueue---end");
}

控制台输出:

截屏2021-03-19 下午6.25.31.png

运行奔溃了!!!

为什么呢?

这是因为主线程中正在执行 syncMainQueue 这个方法,这也是个任务,同步执行的时候,如果我们想要执行追加进来的任务一的时候,由于是同步执行,此时队列中的任务肯定是先执行先添加进来的任务syncMainQueue,等 syncMainQueue执行完了再执行任务一,但是 syncMainQueue 要想执行完,必须等任务一二三全部执行完,所以造成了互相等待的状态,从而导致卡死。

4.5.2 分线程中操作主队列任务同步执行

[NSThread detachNewThreadSelector:@selector(otherThreadSyncMain) toTarget:self withObject:nil];

...

- (void)otherThreadSyncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMainQueue---begin");
    dispatch_queue_t syncMainQueue = dispatch_get_main_queue();
    dispatch_sync(syncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"1---%@",[NSThread currentThread]);      
        }
    });
    dispatch_sync(syncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"2---%@",[NSThread currentThread]);      
        }
    });
    dispatch_sync(syncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              
            NSLog(@"3---%@",[NSThread currentThread]);      
        }
    });
    NSLog(@"syncMainQueue---end");
}

控制台输出:

截屏2021-03-19 下午6.35.36.png

所有任务都在打印的begin和end之间执行,同步任务需要等待队列中的上一个任务执行结束。另外任务的执行都在主线程,说明子线程的任务都调度到了主线程中去执行,说明 主队列的任务都会放到主线程中执行。

之所以不会卡住,是因为otherThreadSyncMain这个任务时放在子线程中执行的,而后面追加的任务时调度到主线程去执行了一下。

4.6 主队列任务异步执行

- (void)asyncMainQueue {
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    NSLog(@"asyncMain---begin");
    dispatch_queue_t asyncMainQueue = dispatch_get_main_queue();
    dispatch_async(asyncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(asyncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@",[NSThread currentThread]);      
        }
    });
    dispatch_async(asyncMainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    NSLog(@"syncMainQueue---end");
}

控制台输出:

截屏2021-03-19 下午6.44.44.png

任务的执行都在 begin 和 end 之后,说明异步执行任务,任务之间的执行不做等待,所有任务的执行都在主线程执行,没有开启新的线程,再次说明主队列的任务都会在主线程执行。

异步执行主队列中的任务和异步执行串行队列中的任务两者的区别是:前者不开启新的线程,所有任务都在主线程执行,而后者是开启一条新的线程,任务的执行均在这条新的线程执行。

4.7 全局队列 用法和并发队列类似

线程间的调度可以看错线程间通信。


- (void)globalQueue {
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_async(globalQueue, ^{
        // 异步追加任务
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
        // 调度到主线程,比如去刷新UI
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
    });
}

4.8 在子线程中执行 performSelector: withObject: afterDelay:

我们在一个异步执行的并发队列,执行 performSelector 函数,会怎样?

dispatch_queue_t queue = dispatch_queue_create("com.app.cn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(hahaSelect) withObject:nil afterDelay:2.0f];//延迟调用会阻塞当前线程,两秒后输出“2”
        NSLog(@"2");
    });
    
- (void)hahaSelect {
    NSLog(@"haha");
}

控制台直接输出了:

2021-04-09 13:37:19.765256+0800 TestData[3335:125405] 1
2021-04-09 13:37:19.765532+0800 TestData[3335:125405] 2

并没有去调用 hahaSelect方法,为什么么?

因为我们在子线程用去调用 performSelector: withObject: afterDelay: 方法时,相当于开启一个 timer 添加到 runloop 中,但是由于子线程中的runloop 默认不是开启的状态,所以 我们需要调用 [[NSRunLoop currentRunLoop] run] 使子线程的 runloop 跑起来。

另外,需要注意 [[NSRunLoop currentRunLoop] run] 必须添加到 performSelector: withObject: afterDelay: 后面,因为如果添加到前面去run的话,此刻 runloop里面既没有 timer,也没有source时间,runloop还是跑不起来的。

所以,整理代码后如下:

dispatch_queue_t queue = dispatch_queue_create("com.app.cn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(hahaSelect) withObject:nil afterDelay:2.0f];//延迟调用会阻塞当前线程,两秒后输出“2”
        [[NSRunLoop currentRunLoop] run];//因为子线程里runloop默认是关闭的,需要使runloop动起来
        NSLog(@"2");
    });

其次,如果我们将 performSelector: withObject: afterDelay: 方法替换performSelector: 方法,还需要启动runloop吗?

答案是 不需要,因为 performSelector 只是一个单纯的消息发送,和时间timer没有一点关系。所以不需要添加到子线程的Runloop中也能执行。

4.9 线程安全

先看以下代码:

@property (nonatomic, strong) NSString *target;
......
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 1000 ; i++) {
        dispatch_async(queue, ^{
            self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];//可能会崩溃
        });
    }

这样运行以后,有可能会崩溃,报 EXC_BAD_ACCESS 错误。 先看下MRC下 target 的 set 方法:

- (void)setTarget:(NSString *)target {
    [target retain];//先保留新值
    [_target release];//再释放旧值
    _target = target;//再进行赋值
}

原因是MRC下的 nonatomic 修饰的 targe 的 set 方法,异步的并发队列会导致 过度释放,可以修改为 atomic 是的setter 方法线程安全,或者使用串行队列。

总结

区别同步执行任务异步执行任务
串行队列不开启新线程,任务按先后顺序执行开启一条新线程,任务按顺序执行
并发队列不开启新线程,任务按先后顺序执行开启多条线程,任务同时执行
主队列在主线程中执行会造成卡死,在子线程中执行时会把任务调度到主线程执行,任务按序执行所有任务在主线程执行,任务按序执行

上述表格可以看出:异步任务+非主队列才具备开启线程的能力, 开不开线程取决于执行任务的方式,同步执行不开线程,异步执行开新线程,开几条线程取决于队列,串行开辟一条,并行开辟一条或者多条。

另外,同步执行任务时,任务执行都是在 begin 和 end 之间执行的,说明同步执行任务会造成线程阻塞。异步执行任务的相反

5. GCD 的其他用法

5.1 一次执行函数 dispatch_once

我们常在自定义单例类的时候会用到这个函数。 GCD的 一次执行函数是的App在整个运行周期内,只会被执行一次。

- (void)gcd_dispatch_only_once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
    //此处执行的代码只会被执行一次,线程安全。
 });
}

5.2 延时方法 dispatch_after

- (void)gcd_dispatch_after {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"aGCD_dispatch_after---begin");
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_after(time, queue, ^{
        // 3.0秒后异步追加任务代码到主队列,并开始执行
        NSLog(@"after---%@",[NSThread currentThread]);  
    });
}

5.3 栅栏函数 dispatch_barrier_async

简单举个使用场景:有A/B/C/D四个任务,我们需要A和B先同时执行,也就是异步执行,等AB都完成后,CD再开始异步执行任务,那么我们需要栅栏函数在异步执行的队列中,分隔开AB和CD两组任务已达到场景使用要求。

- (void)GCD_dispatch_barrier_async {
    NSLog(@"asyncBarrier---start");
    dispatch_queue_t concrrentQueue = dispatch_queue_create("com.app.cp", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concrrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"A---%@",[NSThread currentThread]);
    });
    dispatch_async(concrrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"B---%@",[NSThread currentThread]);
    });
    dispatch_barrier_async(concrrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"C---%@",[NSThread currentThread]);
    });
    dispatch_async(concrrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"D---%@",[NSThread currentThread]);
    });
    dispatch_async(concrrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"E---%@",[NSThread currentThread]);
    });
    NSLog(@"asyncBarrier---end");
}

控制台输出为:

截屏2021-03-19 下午11.05.23.png

我们这里是在并行队列中使用的,那之前我们说的全局队列也是一种并行队列,那么我们把上述例子的并行队列改为:

dispatch_queue_t concrrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

控制台输出为:

截屏2021-03-19 下午11.12.16.png

发现栅栏函数并没有在异步执行全局队列中任务的这种情景中生效。

所以:在全局队列使用栅栏方法,是无法进行拦截的。是无效的

5.4 队列组 dispatch_group 和 dispatch_group_notify

简单举个使用场景:我们需要异步执行两个耗时的下载图片任务,我需要等两个任务的图片都下载完成以后,再在主线程把图片展示出来,我们就需要GCD的队列组。

使用步骤是:

第一步:先创建一个队列组

dispatch_group_t group = dispatch_group_create();

第二步:添加任务到一个队列中,然后将这个队列添加到队列组中

// group : 上面创建的队列组
// queue : 需要被添加进队列组的队列/需要添加任务的队列
// block : 任务
dispatch_group_async(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);

第三步:使用 dispatch_group_notify 进行调度到主队列中。

dispatch_group_notify(group, dispatch_get_main_queue(), ^{};

那么整个场景的业务处理代码就是:


- (void)GCD_dispatch_group0 {
    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), ^{
        //任务A
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"A---%@",[NSThread currentThread]);
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //任务B
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B---%@",[NSThread currentThread]);
        }
    });
    
    //通过 dispatch_group_notify 调度到主队列去执行任务
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"A和B都完成了,该我啦!");
    });
    NSLog(@"group---end");
}

控制台输出为:

截屏2021-03-19 下午11.40.51.png

5.5 信号量 dispatch semaphore

GCD 的信号量使用过程中会遇到三个函数分别是:

dispatch_semaphore_creat(x) :创建一个Semaphore并初始化信号的总量为x。

dispatch_semaphore_signal:发送一个信号,让信号总量加1。

dispatch_semaphore_wait:可以使总信号量减1,当信号总量小于0时就会一直等待(阻塞所在线程),否则就可以正常执行。

信号量在开发过程中常用的作用有两个:

第一:是保持线程同步,比如将异步执行任务转换为同步执行任务。

第二:是对线程加锁,保证数据的安全。

5.5.1 dispatch semaphore 线程同步/异步改为同步

/**
 * semaphore 线程同步
 */1-(void)testDispatchSemaphore0
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);  //创建一个信号量初始值为1
    NSMutableArray *array = [NSMutableArray array];    
    for (int index = 0; index < 10; index++) {        
        dispatch_async(queue, ^(){ 
            NSLog(@"quene :%d", index);
            // dispatch_semaphore_wait 会对信号量减1,减1后此时信号量为0,当下一个循环执行到这里的时候,再对信号量减1会变成小于零,就会处于等待状态从而不会执行下面的代码
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);            
            NSLog(@"addd :%d", index);//2            
            [array addObject:[NSNumber numberWithInt:index]];
            //在 addObject后,通过dispatch_semaphore_signal将信号量加1,此刻信号总量为1,下次循环可以正常进行。
            dispatch_semaphore_signal(semaphore);            
        });        
    }
}

例2:

- (void)testDispatchSemaphore1 {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);// 1.给信号量初值为0
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务1
        [NSThread sleepForTimeInterval:2];              
        NSLog(@"1---%@",[NSThread currentThread]);      
        number = 100;
        // 3. 执行到这里时,会对信号量加1,然后dispatch_semaphore_wait后代码就可以正常执行。
        dispatch_semaphore_signal(semaphore);
    });
    
    // 2.异步执行任务不做等待,当执行到这里时,会对信号量减1,造成线程阻塞等待
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}
控制台输出:semaphore---end,number = 100

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) 由于这里会对信号量减1,如果减一后信号量的值小于0,就会持续等待。写在这几代码后的代码也就不会执行。

5.5.2 dispatch semaphore 线程安全/为线程加锁

常见的两个窗口卖火车票的例子:

#pragma mark - 线程安全 买票问题
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    _semaphore = dispatch_semaphore_create(1);
    self.ticketSurplusCount = 20;
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    __weak __typeof(self)weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

- (void)saleTicketSafe {
    while (1) {//真
        // 相当于上锁
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); //_semaphore 由初始量1减一变为0,在异步执行情况下,后者执行到这一步,在semphore = 0时会处于等待状态,从而达到上锁的目的
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else {
            NSLog(@"所有火车票均已售完");
            // 相当于解锁
            dispatch_semaphore_signal(_semaphore);
            break;
        }
        // 相当于解锁
        dispatch_semaphore_signal(_semaphore);
    }
}

控制台输出:

截屏2021-03-20 上午12.30.15.png

补充上述买票加锁问题

互斥锁

将上述saleTicketSafe 方法替换为:

- (void)saleTicketSafe {
    //互斥锁
    while (1) {
            // 相当于加锁
            @synchronized(self) {
                if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
                    self.ticketSurplusCount--;
                    NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", _ticketSurplusCount, [NSThread currentThread]]);
                    [NSThread sleepForTimeInterval:0.2];
                } else { //如果已卖完,关闭售票窗口
                    NSLog(@"所有火车票均已售完");
                    break;
                }
            }
        }
}

控制台和上面自旋锁一致。

添加NSLock

将上述saleTicketSafe 方法替换为:

- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
         [_lock lock];
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", _ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }
        [_lock unlock];
    }
}

6. 部分场景问题

部分场景问题,除了上面买票的问题之外,还总结了其他几个。

6.1 GCD如何控制最大并发数(使用信号量)

通过信号量的使用,我们可以控制GCD的最大并发数。

- (void)GCD_maxConcurrentOperationCount {
    dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);//最大并发数4
    for (int a = 0;a<10 ; a++) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [NSThread sleepForTimeInterval:1];
            NSLog(@"GCD控制最大并发数----%d",a);
            dispatch_semaphore_signal(semaphore);
        });
    }
}

打印输出为:

2021-04-09 16:15:55.278003+0800 TestData[5689:235489] GCD控制最大并发数----2----<NSThread: 0x600003604080>{number = 9, name = (null)}
2021-04-09 16:15:55.278194+0800 TestData[5689:235492] GCD控制最大并发数----1----<NSThread: 0x60000366fb80>{number = 5, name = (null)}
2021-04-09 16:15:55.278200+0800 TestData[5689:235488] GCD控制最大并发数----0----<NSThread: 0x600003646080>{number = 7, name = (null)}
2021-04-09 16:15:55.278415+0800 TestData[5689:235749] GCD控制最大并发数----3----<NSThread: 0x600003604040>{number = 10, name = (null)}
2021-04-09 16:15:56.282437+0800 TestData[5689:235752] GCD控制最大并发数----6----<NSThread: 0x600003604bc0>{number = 11, name = (null)}
2021-04-09 16:15:56.282436+0800 TestData[5689:235751] GCD控制最大并发数----5----<NSThread: 0x600003611480>{number = 14, name = (null)}
2021-04-09 16:15:56.282436+0800 TestData[5689:235753] GCD控制最大并发数----7----<NSThread: 0x600003608c00>{number = 12, name = (null)}
2021-04-09 16:15:56.282437+0800 TestData[5689:235750] GCD控制最大并发数----4----<NSThread: 0x600003604140>{number = 13, name = (null)}
2021-04-09 16:15:57.286603+0800 TestData[5689:235755] GCD控制最大并发数----9----<NSThread: 0x600003638c00>{number = 16, name = (null)}
2021-04-09 16:15:57.286600+0800 TestData[5689:235754] GCD控制最大并发数----8----<NSThread: 0x600003608d80>{number = 15, name = (null)}

可以看到这里确实是达到了控制最大并发数为4的操作,但是,这样操作创建了10条不同的线程,毕竟开启新的线程是需要消耗内存的,主线程大约1M,子线程是512Kb,所以,我如果把 dispatch_semaphore_wait 位置优化一下会怎样?

- (void)GCD_maxConcurrentOperationCount {
    dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);//最大并发数4
    for (int a = 0;a<10 ; a++) {
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"GCD控制最大并发数----%d----%@",a,[NSThread currentThread]);
            dispatch_semaphore_signal(semaphore);
        });
    }
}

打印输出为:

2021-04-09 16:19:50.708440+0800 TestData[5745:238827] GCD控制最大并发数----0----<NSThread: 0x60000395d9c0>{number = 8, name = (null)}
2021-04-09 16:19:50.708459+0800 TestData[5745:238921] GCD控制最大并发数----2----<NSThread: 0x60000395dd40>{number = 10, name = (null)}
2021-04-09 16:19:50.708459+0800 TestData[5745:238920] GCD控制最大并发数----1----<NSThread: 0x600003944000>{number = 9, name = (null)}
2021-04-09 16:19:50.708482+0800 TestData[5745:238922] GCD控制最大并发数----3----<NSThread: 0x60000393bf00>{number = 11, name = (null)}
2021-04-09 16:19:51.714210+0800 TestData[5745:238922] GCD控制最大并发数----4----<NSThread: 0x60000393bf00>{number = 11, name = (null)}
2021-04-09 16:19:51.714210+0800 TestData[5745:238920] GCD控制最大并发数----6----<NSThread: 0x600003944000>{number = 9, name = (null)}
2021-04-09 16:19:51.714210+0800 TestData[5745:238827] GCD控制最大并发数----5----<NSThread: 0x60000395d9c0>{number = 8, name = (null)}
2021-04-09 16:19:51.714210+0800 TestData[5745:238921] GCD控制最大并发数----7----<NSThread: 0x60000395dd40>{number = 10, name = (null)}
2021-04-09 16:19:52.715640+0800 TestData[5745:238827] GCD控制最大并发数----9----<NSThread: 0x60000395d9c0>{number = 8, name = (null)}
2021-04-09 16:19:52.715640+0800 TestData[5745:238921] GCD控制最大并发数----8----<NSThread: 0x60000395dd40>{number = 10, name = (null)}

可以看到整个业务下来只创建了4个不同的线程,这样可以节约内存消耗。

6.2 GCD如何取消任务的执行

GCD不像 NSOperation 一样可以通过调用 cancel 来取消未执行的操作,但是GCD可以通过 dispatch_block_t()dispatch_block_cancel() 的使用来取消还未执行的任务操作。

注意:无论是 GCD 还是 NSOperation 都不能取消正在执行中的任务,只能取消还未执行的任务。

- (void)GCD_cancelQueueOperation {
    dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_block_t blockOpt1 = dispatch_block_create(0, ^{
        
        NSLog(@"block1 %@",[NSThread currentThread]);
    });
    dispatch_block_t blockOpt2 = dispatch_block_create(0, ^{
        NSLog(@"block2 %@",[NSThread currentThread]);
    });
    dispatch_block_t blockOpt3 = dispatch_block_create(0, ^{
        NSLog(@"block3 %@",[NSThread currentThread]);
    });
    dispatch_async(queue, blockOpt1);
    dispatch_async(queue, blockOpt2);
    dispatch_block_cancel(blockOpt3);//取消blockOpt3的任务的执行
}

另外,还可以通过外部变量的标志,来确定是否需要取消操作,如果需要取消,直接reture

- (void)GCD_cancelQueueOperation2 {
    dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"block1 %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"block2 %@",[NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        self.isCancel = YES;
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1];
        if (self.isCancel) {
            return;
        }
        NSLog(@"block3 %@",[NSThread currentThread]);
    });
}

6.3 如何下载多张图片后统一处理(使用队列组或者栅栏函数)

第一种方法:使用队列组 dispatch_group_t 来实现,然后通过 dispatch_group_notify 调度到主队列进行整合图片

//添加队列组异步执行后使用dispatch_group_notify 进行整合
- (void)groupDownloadImgThenUseNotifyReorganize {
    dispatch_group_t group = dispatch_group_create();
    for (int i = 0; i < 20; i ++) {
        dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_async(group, queue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"下载第%d张图片耗时0.5秒  当前线程--%@",i,[NSThread currentThread]);
        });
    }
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"图片都下载好了,我要开始整合");
    });
}

第二种方法:使用栅栏函数,通过 dispatch_barrier_async 调度到主队列进行整合图片

dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    for (int a = 0; a < 20; a ++) {
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"下载图片--%d",a);
        });
    }
    dispatch_barrier_async(queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"下载完成20张图片--%@",[NSThread currentThread]);
        });
    });

6.4 用不少于一个线程按照顺序打印1到20(使用@synchronized()互斥锁或则信号量自旋锁)

这个有两个要点,一个是“不少于一个线程”,一个是“按顺序”,那么,我可以通过创建两个异步并发队列,来保证不少于一个线程,通过加锁的方式,来对其执行加锁。加锁的方式在上面已经知道常用的有互斥锁和信号量自旋锁。

第一种方法,@synchronized()互斥锁

 dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        [self log];
    });
    
dispatch_queue_t queue2 = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue2, ^{
        [self log];
    });
    
......

- (void)log {
    static int i = 1;
    while (1) {
        @synchronized (self) {
            if (i <= 100) {
                NSLog(@"i = %d  当前线程%@",i,[NSThread currentThread]);
                i ++;
            } else {
                break;
            }
        }
    }
}
    

第二种方法,使用信号量 自旋锁

- (void)orderLog2 {
   self.orderSemaphore = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        [self log2];
    });
    
    dispatch_queue_t queue2 = dispatch_queue_create("com.app.gz", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue2, ^{
        [self log2];
    });
}

- (void)log2 {
    static int i = 1;
    dispatch_semaphore_wait(self.orderSemaphore, DISPATCH_TIME_FOREVER);
    while (1) {//while(1)其中1代表一个常量表达式,它永远不会等于0。循环会一直执行下去。除非你设置break等类似的跳出循环语句循环才会中止
        if (i <= 100) {
            NSLog(@"i = %d  当前线程%@",i,[NSThread currentThread]);
            i ++;
        } else {
            dispatch_semaphore_signal(self.orderSemaphore);
            break;//停止循环
        }
        dispatch_semaphore_signal(self.orderSemaphore);
    }
    
}

以上,就是对 GCD 的使用的一个大致的学习总结。