美文网首页
第三十节—GCD(二)

第三十节—GCD(二)

作者: L_Ares | 来源:发表于2020-11-26 01:32 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    前一节介绍了GCD的基本概念,尤其是对队列和函数的概念,组合,有了认知,本节则从更贴近实际使用情况的一些地方来探索GCD,介绍一些GCD的常用方法。

    一、GCD栅栏函数

    1. 基本概念

    • 英文全称 : Dispatch Barrier

    • 中文全称 : 栅栏函数

    • 作用 : 分割并阻塞队列的执行,以保证队列中某些任务的执行顺序。

    • 限制 : 想要栅栏函数阻塞队列,则必须确保要阻塞的队列和栅栏函数添加任务的队列是同一个队列。并且,不可以使用全局并发队列。

    • 常见函数 :

    阻塞队列,但是不阻塞线程
    dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    阻塞队列,又阻塞线程
    dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
    
    图1.1.0.png

    2. 简单使用

    2.1 异步栅栏函数 :
    - (void)jd_gcd_barrier
    {
        
        NSLog(@"\n开始 : %@",[NSThread currentThread]);
        
        //创建一个并发队列
        dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_barrier_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        
        //异步添加任务1
        dispatch_async(concurrent_queue, ^{
            NSLog(@"\n任务1 --- 当前线程 : %@",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:2.f];
        });
        
        //异步添加任务2
        dispatch_async(concurrent_queue, ^{
            NSLog(@"\n任务2 --- 当前线程 : %@",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:2.f];
        });
        
        //栅栏函数
        dispatch_barrier_async(concurrent_queue, ^{
            NSLog(@"\n栅栏函数 --- 当前线程 : %@",[NSThread currentThread]);
        });
        
        //异步添加任务3
        dispatch_async(concurrent_queue, ^{
            NSLog(@"\n任务3 --- 当前线程 : %@",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:2.f];
        });
        
        //异步添加任务4
        dispatch_async(concurrent_queue, ^{
            NSLog(@"\n任务4 --- 当前线程 : %@",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:2.f];
        });
        
        NSLog(@"\n结束 : %@",[NSThread currentThread]);
        
    }
    

    执行结果 :

    图1.2.0.png
    2.2 同步栅栏函数 :

    只需要将上述代码的栅栏函数去掉一个a,就会变成同步栅栏函数 :

        dispatch_barrier_sync(concurrent_queue, ^{
            NSLog(@"\n栅栏函数 --- 当前线程 : %@",[NSThread currentThread]);
        });
    

    其他代码和上述一模一样。

    执行结果 :

    图1.2.1.png

    3. 举例

    这是一个简单的逻辑,假设这是异步处理一些耗时操作。

    - (void)jd_gcd_barrier_global_queue
    {
        
        NSLog(@"开始");
        //获取到全局并发队列
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //创建一个并发队列
        dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_barrier_global_queue_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        
        for (int i = 0; i < 1000; i++) {
            dispatch_async(concurrent_queue, ^{
                [self.myMutArr addObject:[NSString stringWithFormat:@"%d",i]];
            });
        }
        
        NSLog(@"结束");
        
    }
    

    结果 :

    图1.3.0.png

    会出现如图1.3.0中所示的错误,原因很简单,就是因为异步添加的任务会同时去访问可变数组,导致可变数组出现错误。

    这里就可以使用栅栏函数,改成如下所示的代码 :

    - (void)jd_gcd_barrier_global_queue
    {
        
        NSLog(@"开始");
        //获取到全局并发队列
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //创建一个并发队列
        dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_barrier_global_queue_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        
        for (int i = 0; i < 1000; i++) {
            dispatch_async(concurrent_queue, ^{
                dispatch_barrier_async(concurrent_queue, ^{
                    [self.myMutArr addObject:[NSString stringWithFormat:@"%d",i]];
                });
            });
        }
        
        NSLog(@"结束");
        
    }
    

    但是,如果你使用全局并发队列的话,则依然会出错。

    原因是全局并发队列中也处理系统级别的任务,突然出现一个栅栏函数对全局并发队列进行阻塞,可能会阻塞系统级别的任务的进行。

    结论 :

    • 栅栏函数不可以用于全局并发队列。
    • 对于异步栅栏函数,只会阻塞指定队列中的任务,在栅栏函数添加之后添加的任务,必须在栅栏函数异步添加的任务之后执行。
    • 对于同步栅栏函数,即会阻塞指定的队列,也会因为同步,阻塞当前的线程。所有在同步栅栏函数之后添加进当前线程的任务,都会在同步栅栏函数之后执行。

    二、GCD延时方法

    1. 延时方法 : dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

    2. 延时方法是将任务延时异步添加到队列中,也就是说是可以调整任务加入队列的时机。

    3. 但是,延时方法是延时添加任务,不是准确的延时执行任务,只能大致的保证任务在后面进行,而不是肯定。

    - (void)jd_gcd_after
    {
        
        NSLog(@"开始 --- %@",[NSThread currentThread]);
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2秒后添加的任务到线程 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"结束 --- %@",[NSThread currentThread]);
        
    }
    

    三、GCD一次性代码

    一次性代码在整个程序的运行过程中只执行一次的代码,即使在多线程的情况下,一次性代码也可以保证线程安全,常见的用法会用来做单例。

    • 函数 : dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block);
    - (void)jd_gcd_once
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
           //在这里执行的代码都是线程安全的,并且整个程序只运行一次
        });
    }
    

    四、GCD快速迭代方法

    1. 函数 : dispatch_apply(size_t iterations, dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue, DISPATCH_NOESCAPE void (^block)(size_t));

    2. 特点 : GCD的快速迭代方法和for的循环遍历思想类似,dispatch_apply会按照你指定的次数,依次的将指定的任务添加到指定的队列中,并且会等待全部任务的执行完成,再执行下面的任务。也就是说,GCD的快速迭代方法可以阻塞线程。

    3. 注意 :

    1. 不要主队列 + 主线程 + 快速迭代方法,这样依然会产生死锁,原因和主队列 + 主线程 + 同步函数是一样的。
    2. 如果使用自己创建的串行队列的话,那么就和for循环一样了,按照顺序同步执行,但是这样的话,使用for循环是不是更简单。
    - (void)jd_gcd_apply
    {
        NSLog(@"开始 --- %@",[NSThread currentThread]);
        //全局并发队列,会开启线程
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //快速迭代方法,但是不能在主线程中这么使用,会造成线程死锁
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        //串行队列,不会开启线程
        dispatch_queue_t serial_queue = dispatch_queue_create("jd_gcd_apply_serial_queue", DISPATCH_QUEUE_SERIAL);
        //快速迭代
        dispatch_apply(6, serial_queue, ^(size_t index) {
            [NSThread sleepForTimeInterval:2.f];
            NSLog(@"%zd --- %@",index,[NSThread currentThread]);
        });
        NSLog(@"结束 --- %@",[NSThread currentThread]);
    }
    

    五、GCD调度组

    调度组理解起来就比较容易了,调度组可以把几个任务组合成一组任务,相当于多合一。比如,请求多个接口,然后组合请求的数据,等这些执行完毕之后,再回调主线程刷新UI,一般情况下,异步添加任务到调度组使用的比较多,因为同步会阻塞线程,阻碍其他的任务的执行,还可能会造成死锁。

    一般常见的调度组使用函数有如下几个 :

    • 调度组的类型 : dispatch_group_t

    • 创建一个调度组 : dispatch_group_create();

    • 异步添加任务到调度组 : dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

    • 调度组执行完毕后,回调 : dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

    • 进组 : dispatch_group_enter(dispatch_group_t group);

    • 出组 : dispatch_group_leave(dispatch_group_t group);

    • 等待调度组 : dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

    下面我们分类型来分别说他们的使用。

    5.1 dispatch_group_notify

    - (void)jd_gcd_group_async
    {
        //创建一个调度组
        dispatch_group_t group = dispatch_group_create();
        
        //获取全局并发队列
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //获取主队列
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        
        //将任务1异步的放入到调度组中
        dispatch_group_async(group, global_queue, ^{
            //为了看清楚效果,这里添加一个延时执行,模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //这里放任务1
            NSLog(@"我是任务1 --- %@",[NSThread currentThread]);
        });
        
        //将任务2异步的放入到调度组中
        dispatch_group_async(group, global_queue, ^{
            //为了看清楚效果,这里添加一个延时执行,模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //这里放任务2
            NSLog(@"我是任务2 --- %@",[NSThread currentThread]);
        });
        
        //假设上面是网络请求的耗时操作,那么请求完了数据,我们回主线程更新UI
        dispatch_group_notify(group, main_queue, ^{
            NSLog(@"回到主线程更新UI了 --- %@",[NSThread currentThread]);
        });
    }
    

    执行结果 :

    图5.1.0.png

    结论 :

    当调度组的任务执行完成之后,才会执行dispatch_group_notify中的通知回调。

    5.2 dispatch_group_wait

    阻塞当前线程,等待调度组的任务执行完毕后,线程才可以执行后面的任务。

    - (void)jd_gcd_group_wait
    {
        //创建一个调度组
        dispatch_group_t group = dispatch_group_create();
        //一个队列,类型随意
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //异步添加任务1到调度组中
        dispatch_group_async(group, global_queue, ^{
            //为了看清楚效果,这里添加一个延时执行,模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //这里放任务1
            NSLog(@"我是任务1 --- %@",[NSThread currentThread]);
        });
        
        //异步添加任务2到调度组中
        dispatch_group_async(group, global_queue, ^{
            //为了看清楚效果,这里添加一个延时执行,模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //这里放任务2
            NSLog(@"我是任务2 --- %@",[NSThread currentThread]);
        });
        
        //然后堵住当前的线程,等待调度组执行完毕
        //第一个参数 : 要等待的调度组
        //第二个参数 : 允许等待调度组多长时间,这里写的是一直等待
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        
        //其他的任务
        NSLog(@"我是其他的任务 --- %@",[NSThread currentThread]);
        
    }
    

    执行结果 :

    图5.2.0.png

    结论 :

    dispatch_group_wait会等待指定的调度组完成任务后,再执行后面的任务。相当于阻塞了线程。

    5.3 dispatch_group_enter和dispatch_group_leave

    dispatch_group_enterdispatch_group_leave一定要成对的出现,并且必须先dispatch_group_enter,后才可以dispatch_group_leave

    • dispatch_group_enter : 表示追加一个任务到指定的调度组中。执行一次,调度组中未执行的任务数量就+1。

    • dispatch_group_leave : 表示从指定的调度组中去除一个任务。执行一次,调度组中未执行的任务数量就-1。

    • 只有当调度组中的任务数量为0的时候,才会进入到dispatch_group_notify或者让dispatch_group_wait不再阻塞线程。

    - (void)jd_gcd_enter_and_leave_group
    {
        //创建一个调度组
        dispatch_group_t group = dispatch_group_create();
        
        //得到一个队列
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //获取主队列
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        
        //任务1进入调度组
        dispatch_group_enter(group);
        dispatch_async(global_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印线程
            NSLog(@"我是任务1 --- %@",[NSThread currentThread]);
            //上述的任务1执行完成后,出组
            dispatch_group_leave(group);
        });
        
        //任务2进入调度组
        dispatch_group_enter(group);
        dispatch_async(global_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印线程
            NSLog(@"我是任务2 --- %@",[NSThread currentThread]);
            //上述的任务2执行完成后,出组
            dispatch_group_leave(group);
        });
        
        //等到接收调度组中的任务执行完毕,回调
        dispatch_group_notify(group, main_queue, ^{
            NSLog(@"调度组任务执行完毕 --- %@",[NSThread currentThread]);
        });
        
    }
    

    执行结果 :

    图5.3.0.png

    结论 :

    • dispatch_group_enterdispatch_group_leave必须成对出现。
    • dispatch_group_leave必须要在调度组中存在任务的时候才可以执行,否则会crash。
    • dispatch_group_enterdispatch_group_leave的效果等同于dispatch_group_async

    六、GCD信号量

    1. 基本概念

    • 英文全称 : Dispatch Semaphore

    • 中文全称 : GCD信号量

    • 常用于 :

    1. 把异步任务转换成同步。
    2. 计数设置为1的话,可以保证线程安全。
    • 含义 : 持有计数的信号。

    信号量即是一种计数方式,它通过计数来判断是否阻塞线程。

    • 当信号量 计数 < 0 的时候,会进入等待(阻塞)状态。
    • 当信号量 计数 >= 0 的时候,计数减1,且不等待。

    2. 常用函数

    semaphore.h中(信号量的头文件),我们可以找到官方提供的3个常用函数 :

    (1). 创建信号量

    dispatch_semaphore_t 
    dispatch_semaphore_create(intptr_t value);
    

    参数 :

    • intptr_t value : 信号量的初始值,如果传递一个小于0的值则会导致返回一个NULL

    返回值 :

    • dispatch_semaphore_t : 信号量的类型,是typedef dispatch_semaphore_t

    (2). 信号量+1

    intptr_t
    dispatch_semaphore_signal(dispatch_semaphore_t dsema);
    

    参数 :

    • dispatch_semaphore_t dsema : 信号量,就是上面创建的那个。

    返回值 :

    • 返回值只有两种情况,一种是非0,证明线程已经被唤醒,否则返回0。

    (3). 信号量-1

    intptr_t
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
    

    参数 :

    • dispatch_semaphore_t dsema : 信号量

    • dispatch_time_t timeout : 什么时候超时,详情可以看dispatch_time,常见的有DISPATCH_TIME_NOW立即超时和DISPATCH_TIME_FOREVER永远不超时。

    3. 举例

    按照上面说的常用于来举两个例子。

    3.1 GCD信号量用于异步任务转换同步性质
    - (void)jd_gcd_semaphore
    {
        //创建一个信号量,并且设置初始的信号计数为0
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        //得到一个并发队列
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //定义一个数,拿来举例用
        __block int a = 1;
        
        //定义一个while循环,拿来举例用
        while (a < 10) {
            //异步的向全局并发队列添加任务,让a自增
            dispatch_async(global_queue, ^{
               //这里添不添加模拟耗时的操作都可以
               //[NSThread sleepForTimeInterval:2.f];
                
                //因为是全局并发队列,又是异步添加,所以会开启线程
                //但是因为多个线程会同时访问a的内存,所以如果不做一个类似锁的操作
                //就会出现a最后会比10大,而且不能确定a的大小
                a++;
                NSLog(@"当前 a == %d , 线程 : %@",a,[NSThread currentThread]);
                
                //只有给信号计数+1以后,信号计数才会>=0,才可以放行
                dispatch_semaphore_signal(sem);
            });
            //上面因为是异步添加任务,所以主线程会先走到这里,把信号计数变成-1
            //因为信号计数是-1,是小于0的,所以wait函数所在的主线程会被阻塞
            //不会马上执行while循环
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
            
            //这里定义一个主线程执行的任务,可以看到,wait函数会阻塞这个任务的执行
            NSLog(@"有没有阻塞的住 --- %@",[NSThread currentThread]);
        }
        
    }
    

    执行结果 :

    图6.3.0.png

    结论 :

    1. dispatch_semaphore_wait();函数和dispatch_semaphore_signal();也是成对出现的。
    2. dispatch_semaphore_wait();会使信号计数-1,并在信号计数小于0的时候阻塞它自己所在的线程。
    3. dispatch_semaphore_signal()则会使信号计数+1。
    4. 通过其配合使用,可以将异步任务变成具有同步性质。
    3.2 GCD信号量用于线程安全

    首先,要明白什么是线程安全线程同步

    1. 线程安全 : 一般情况下,如果进程中的某段代码在该进程的多条线程中同时执行,如果每次运行的结果和单线程运行的结果是一致的,那么它就是线程安全的。
    2. 线程同步 : 一般情况下可以理解为,存在两条线程需要配合获得最终结果,设两条线程分别为线程A线程B线程A需要线程B中得到的结果,于是线程A会在执行到某种情况下停止运行,通知线程B执行,并且返回结果给线程A,然后,线程A拿到线程B给它的结果继续运行,获得最终的结果。

    举例 :

    以火车售票的案例来简单举例说明,如何用GCD的信号量来保证线程安全。

    错误代码 :

    - (void)jd_gcd_semaphore_thread_security
    {
        //初始化模拟的火车票数量
        self.train_tickets = 50;
        //初始化售票窗口1
        dispatch_queue_t ticket_sale_queue1 = dispatch_queue_create("ticket_sale_queue1", DISPATCH_QUEUE_SERIAL);
        //初始化售票窗口2
        dispatch_queue_t ticket_sale_queue2 = dispatch_queue_create("ticket_sale_queue2", DISPATCH_QUEUE_SERIAL);
        //初始化信号量
        self.mySem = dispatch_semaphore_create(0);
        
        /**
         同时访问同一个变量 : 票数
         */
        //block里面要用self,记得弱引用,避免循环引用
        __weak typeof(self) weakSelf = self;
        //售票窗口1开始卖票
        dispatch_async(ticket_sale_queue1, ^{
            [weakSelf jd_gcd_semaphore_sale_tickets];
        });
        
        //售票窗口2开始卖票
        dispatch_async(ticket_sale_queue2, ^{
            [weakSelf jd_gcd_semaphore_sale_tickets];
        });
        
    }
    
    //售卖火车票
    - (void)jd_gcd_semaphore_sale_tickets
    {
        //这里while是为了模拟一种一直在卖票的场景,也就是一直访问同一个变量
        while (1) {
            if (self.train_tickets > 0) {
                self.train_tickets--;
                NSLog(@"总票数剩余 : %d --- %@",self.train_tickets,[NSThread currentThread]);
                [NSThread sleepForTimeInterval:0.1];
            }else{
                NSLog(@"所有票售完");
                break;
            }
        }
    }
    

    执行结果 :

    图6.3.1.png

    结论 :

    没有使用信号量的时候,异步修改(多线程)同一个变量会造成变量的错误结果。

    正确代码 :

    //信号量保证线程安全
    - (void)jd_gcd_semaphore_thread_security
    {
        //初始化模拟的火车票数量
        self.train_tickets = 50;
        //初始化售票窗口1
        dispatch_queue_t ticket_sale_queue1 = dispatch_queue_create("ticket_sale_queue1", DISPATCH_QUEUE_SERIAL);
        //初始化售票窗口2
        dispatch_queue_t ticket_sale_queue2 = dispatch_queue_create("ticket_sale_queue2", DISPATCH_QUEUE_SERIAL);
        //初始化信号量,初始给信号量计数为1,这样才能让一条线程通过,如果给0,谁都通过不了
        mySem = dispatch_semaphore_create(1);
        
        /**
         同时访问同一个变量 : 票数
         */
        //block里面要用self,记得弱引用,避免循环引用
        __weak typeof(self) weakSelf = self;
        //售票窗口1开始卖票
        dispatch_async(ticket_sale_queue1, ^{
            [weakSelf jd_gcd_semaphore_sale_tickets];
        });
        
        //售票窗口2开始卖票
        dispatch_async(ticket_sale_queue2, ^{
            [weakSelf jd_gcd_semaphore_sale_tickets];
        });
        
    }
    
    //售卖火车票
    - (void)jd_gcd_semaphore_sale_tickets
    {
        //这里while是为了模拟一种一直在卖票的场景,也就是一直访问同一个变量
        while (1) {
            //这里让信号量的计数-1,就变成了0,如果再有其他人到了这里,当第一个人没有出去的时候
            //信号量再-1,就变成负1了,就会卡死在这一行代码,不会继续执行,直到信号量计数>=0
            dispatch_semaphore_wait(mySem, DISPATCH_TIME_FOREVER);
            if (self.train_tickets > 0) {
                self.train_tickets--;
                NSLog(@"总票数剩余 : %d --- %@",self.train_tickets,[NSThread currentThread]);
                [NSThread sleepForTimeInterval:0.1];
            }else{
                NSLog(@"所有票售完");
                dispatch_semaphore_signal(mySem);
                break;
            }
            //做完了操作了,就要给信号量+1,不然信号量万一还是负数,后面谁都访问不了
            dispatch_semaphore_signal(mySem);
        }
    }
    

    执行结果 :

    图6.3.2.png

    结论 :

    信号量是可以用来解决线程安全的问题的。

    相关文章

      网友评论

          本文标题:第三十节—GCD(二)

          本文链接:https://www.haomeiwen.com/subject/qlipiktx.html