OC底层原理(17)- GCD(栅栏,调度组,信号量,dispatch_source)

2,784 阅读7分钟

练习题

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block int a = 0;
    while (a < 5) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            a++;
        });
    }
    NSLog(@"a = %d", a);
}

A、 a = 0 B、a < 5 C、a = 5 D、a > 5

答案:C、D

答案分析

  • while 循环里面的代码是异步函数,并且整个代码块是一个耗时操作,所以在执行的时候并不会等到整个代码执行完后再进入下一个循环,而是直接进入下一个循环
  • 每次执行一次代码块,因为是异步函数,所以每次都会新开启一个线程来执行任务
  • a 的初始值为 0,当还没执行完 dispatch_async 时,a 都是为 0 ,并且 a++ 会被执行很多次
  • 当 a++ 所在整个代码被第一次执行完时,a = 1,当 a 的值被修改时,由于 a 的地址只有一个,也就是修改的也是同一个内存空间,所以其他线程的 a 的值也会被修改
  • 当 a 的值第一次大于 5 时,就会退出循环,但此时还有很多其他线程任务的 a++ 还没完全执行完,所以在退出循环之后,a 的值还依旧会递增
  • 所以 a 打印的结果就会大于等于 5 。

打印结果:

看到打印结果很明显浪费了很多性能,想直接打到正确的结果还是有一定的风险,要解决这风险我们可以尝试一下使用信号量

//当value 等于 1 时,会起到锁的效果,一次一个执行。
dispatch_semaphore_t sem = dispatch_semaphore_create(1);

// timeout = DISPATCH_TIME_FOREVER ,意味没有回来,就一直等待,加锁的效果
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

//解锁
dispatch_semaphore_signal(sem);

优化代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //dispatch_semaphore_create(long value);当value 等于 1 时,会起到锁的效果,一次一个执行。
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    
    __block int a = 0;
    while (a < 5) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"里面 a = %d----%@", a, [NSThread currentThread]);
            a++;
            dispatch_semaphore_signal(sem);
        });
//            timeout = DISPATCH_TIME_FOREVER ,意味没有回来,就一直等待
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);//

    }
    NSLog(@"a = %d", a);
    
}

打印结果:

答案解析

  • 信号量的目的就是为了每执行一次while 循环的时候,异步代码块也执行一次
  • 由于是这是异步代码块,所以先回执行代码块后面的代码,然后才会开启一个新线程执行块里面的代码,当 a++ 执行完了才是整个代码块执行的结束。
  • 所以在要执行代码块是便加上信号锁,当整个代码块意味结束时,再解锁,于是便不会消耗多余的性能。

栅栏

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

主要起到控制任务执行顺序,起到同步效果

  • dispatch_barrier_async

    前面的任务执行完毕才会到这里

  • dispatch_barrier_sync

    作用相同,但是这个会堵塞线程,影响后面的任务执行

  • 注意,栅栏函数只能控制同一并发队列

基本使用

- (void)demo2{
    
    dispatch_queue_t concurrentQueue = dispatch_queue_create("janice", DISPATCH_QUEUE_CONCURRENT);
    
    /* 1. 异步函数 */
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    
    /* 2. 栅栏函数 */
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    
     /* 3. 异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加载那么多,喘口气!!!");
    });
    
    NSLog(@"************起来干!!");
}

打印结果:

  • 因为 1,2,3 都是异步函数,所以先打印了 ‘起来干’
  • 因为 2 是栅栏函数,所以要等 1 执行玩了之后才会执行 2 , 3 要等栅栏函数执行完了才能被执行。

实际使用场景一

- (void)demo3{
    
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

    // signal -- 线程BUG
    for (int i = 0; i<2000; i++) {
        dispatch_async(concurrentQueue, ^{
            NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
            NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            [self.mArray addObject:image];
        });
    }
}

打印结果

有个问题,添加了2000张图片,但是实际打印效果只有 1997,这是为什么呢,说明线程不安全。

优化办法,在图片加入到数组的时候,添加一个栅栏

- (void)demo3{
    
    // 顺序执行
    // 线程安全
    
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

    // signal -- 线程BUG
    for (int i = 0; i<2000; i++) {
        dispatch_async(concurrentQueue, ^{
            NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
            NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            
            dispatch_barrier_async(concurrentQueue, ^{
                [self.mArray addObject:image];
            });
        });
    }
}

打印结果:

就是添加一个栅栏来保障其线程安全

实际使用场景二

将自定义并发队列换成全局并发队列。

- (void)demo3{
    
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);

    // signal -- 线程BUG
    for (int i = 0; i<2000; i++) {
        dispatch_async(concurrentQueue, ^{
            NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
            NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            
            dispatch_barrier_async(concurrentQueue, ^{
                [self.mArray addObject:image];
            });
        });
    }
}

崩溃了,使用栅栏函数的时候,一定要注意,一定是自定义的并发队列,全局队列是整个系统都在用,在这了使用的情景会就会造成堵塞,所以就会崩溃

总结:

  • 栅栏函数能够起到同步效果
  • 栅栏函数还能起到安全性的效果
  • 堵塞队列
  • 在堵塞队列的时候要使用自己的队列不要使用全局的
  • 使用同一个队列,才能起到效果

调度组

作用:控制任务执行顺序

  • dispatch_group_create 创建组
  • dispatch_group_async 进组任务
  • dispatch_group_notify 进组任务执行完毕通知
  • dispatch_group_wait 进组任务执行等待时间
  • dispatch_group_enter 进组
  • dispatch_group_leave 出组
  • 注意搭配使用

基本使用

- (void)groupDemo{
    
    //创建调度组
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);

    // SIGNAL
    dispatch_group_async(group, queue, ^{
        NSString *logoStr = @"http://p.qpic.cn/qqcourse/QFzQYCgCrxlq7n5Jats36WGb4wxzJIYmFplnUUgdxk4gy66xicbmGCDM5DXDuVNQP/";
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
        UIImage *image = [UIImage imageWithData:data];
        [self.mArray addObject:image];
    });
    
    dispatch_group_async(group, queue1, ^{
        sleep(2);
        NSString *logoStr = @"https://www.baidu.com/img/baidu_resultlogo@2.png";
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
        UIImage *image = [UIImage imageWithData:data];
        [self.mArray addObject:image];
    });
    __block UIImage *newImage = nil;
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"数组个数:%ld",self.mArray.count);
        for (int i = 0; i<self.mArray.count; i++) {
            UIImage *waterImage = self.mArray[i];
            newImage = [KC_ImageTool kc_WaterImageWithWaterImage:waterImage backImage:newImage waterImageRect:CGRectMake(20, 100*(i+1), 100, 40)];
        }
        self.imageView.image = newImage;
    });

}

信号量

dispatch_semaphore_t
  • dispatch_semaphore_create 创建信号量
  • dispatch_semaphore_wait 信号量等待
  • dispatch_semaphore_signal 信号量释放
  • 控制 GCD 并发数

基本使用

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 信号量 -- gcd控制并发数
    // 同步
    //总结:由于设定的信号值为3,先执行三个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过3
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    //任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务1");
        sleep(1);
        NSLog(@"任务1完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务2");
        sleep(1);
        NSLog(@"任务2完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务3");
        sleep(1);
        NSLog(@"任务3完成");
        dispatch_semaphore_signal(semaphore);
    });
}

打印结果:

dispatch_source

  • 其 CPU 负荷非常小,尽量不占用资源
  • 联结的优势

在任一线程上调用它的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 实现定义好的句柄(可以把句柄简单理解为一个 block)这个过程叫 Custom event ,用户事件,是dispatch source 支持处理的一种事件。

句柄是一种指向指针的指针,它指向的就是一个类或者结构,它和系统有很密切的关系

HINSTANCE(实例句柄),HBITMAP(位图句柄),HDC(设备表述句柄),HICON(图标句柄)等。这当中还有一个通用的句柄,就是 HANDLE,比如下面的语句:

  • dispatch_source_create 创建源
  • dispatch_source_set_event_handler 设置源事件的回调
  • dispatch_source_merge_data 源事件设置数据
  • dispatch_source_get_data 获取源事件数据
  • dispatch_resume 继续
  • dispatch_suspend 挂起

基本使用

ViewController.m

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;

@property (nonatomic, assign) NSUInteger totalComplete;
@property (nonatomic) BOOL isRunning;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.totalComplete = 0;
    
    self.queue = dispatch_queue_create("com.tz.cn.janice", 0);
    
     第一个参数:dispatch_source_type_t type为设置GCD源方法的类型,前面已经列举过了。
     第二个参数:uintptr_t handle Apple的API介绍说,暂时没有使用,传0即可。
     第三个参数:unsigned long mask Apple的API介绍说,使用DISPATCH_TIMER_STRICT,会引起电量消耗加剧,毕竟要求精确时间,所以一般传0即可,视业务情况而定。
     第四个参数:dispatch_queue_t _Nullable queue 队列,将定时器事件处理的Block提交到哪个队列之上。可以传Null,默认为全局队列。注意:当提交到全局队列的时候,时间处理的回调内,需要异步获取UI线程,更新UI...不过这好像是常识,又啰嗦了...
     */
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    
    // 保存代码块 ---> 异步 dispatch_source_set_event_handler()
    // 设置取消回调 dispatch_source_set_cancel_handler(dispatch_source_t source,dispatch_block_t _Nullable handler)
    // 封装我们需要回调的触发函数 -- 响应
    dispatch_source_set_event_handler(self.source, ^{
        
        NSUInteger value = dispatch_source_get_data(self.source); // 取回来值 1 响应式
        self.totalComplete += value;
        NSLog(@"进度:%.2f", self.totalComplete/100.0);
        self.progressView.progress = self.totalComplete/100.0;
    });
    
    self.isRunning     = YES;
    dispatch_resume(self.source);
    
    - (IBAction)didClickStartOrPauseAction:(id)sender {

    if (self.isRunning) {// 正在跑就暂停
        dispatch_suspend(self.source);
        dispatch_suspend(self.queue);// mainqueue 挂起
        self.isRunning = NO;
        [sender setTitle:@"暂停中..." forState:UIControlStateNormal];
    }else{
        dispatch_resume(self.source);
        dispatch_resume(self.queue);
        self.isRunning = YES;
        [sender setTitle:@"加载中..." forState:UIControlStateNormal];
    }
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    NSLog(@"点击开始加载");
    
    for (NSUInteger index = 0; index < 100; index++) {
        dispatch_async(self.queue, ^{
            if (!self.isRunning) {
                NSLog(@"暂停下载");
                return ;
            }
            sleep(2);

            dispatch_source_merge_data(self.source, 1); // source 值响应
        });
    }
}

打印结果: