美文网首页
iOS 底层原理 - GCD应用

iOS 底层原理 - GCD应用

作者: yan0_0 | 来源:发表于2020-10-14 11:47 被阅读0次

先看一个面试题

__block int a = 0;
    while (a<5) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"里面 a=%d--%@",a,[NSThread currentThread]);
            a++;
        });
    }
    NSLog(@"外面 a=%d",a);

A: 0 B: <5 C: =5 D: >5
结果为CD,=5或者>5。注意异步队列在while循环里,所以不能往下走。while里面开启异步耗时操作,while正常循环,可能一个循环过后,添加到队列里面的任务还没执行,a还没有++,所以当a=5的时候while里面循环了多少次不确定,但是当a=5的时候才会终止循环,当终止循环后,代码会往下执行,然后打印a,但是终止循环到打印a的这段时间可能并发队列里面的任务有几个正好执行完了,这样就会导致a的值又变大了,所以从外面打印的a大于或者等于5.


屏幕快照 2020-09-23 下午2.24.16.png

这样导致的问题有两个
1.浪费这么多性能
2.最终的a值无法确定

下面我们用信号量的方法处理一下

信号量 dispatch_semaphore_t

通过信号量可以控制不同线程任务的执行顺序和依赖关系,从而达到线程同步的目的。
信号量的使用主要有3个方法来搭配。
1.dispatch_semaphore_create(value):此方法是用来创建信号量,通常加锁的操作时,此时入参0。
2.dispatch_semaphore_wait(): 此方法是等待信号量,会对信号量减1(value - 1),当信号量 < 0时,会阻塞当前线程,等待信号(signal),当信号量 >= 0时,会执行wait后面的代码。
3.dispatch_semaphore_signal(): 此方法是信号量加1,当信号量 >= 0 会执行wait之后的代码

这3个方法必须搭配使用,缺一不可。
dispatch_semaphore_wait()和dispatch_semaphore_signal()是成对使用的。
接下来我们用信号量加锁解决上面的问题,具体代码如下:

dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    __block int a = 0;
    while (a<10) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"里面 a=%d--%@",a,[NSThread currentThread]);
            a++;
            dispatch_semaphore_signal(sem);
        });
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
    NSLog(@"外面 a=%d",a);

打印结果如下:

2020-10-12 21:40:53.886513+0800 001---函数与队列[10182:3358757] 里面 a=0--<NSThread: 0x600001b696c0>{number = 3, name = (null)}
2020-10-12 21:40:53.886523+0800 001---函数与队列[10182:3358751] 里面 a=0--<NSThread: 0x600001b6cb80>{number = 6, name = (null)}
2020-10-12 21:40:53.886748+0800 001---函数与队列[10182:3358757] 里面 a=1--<NSThread: 0x600001b696c0>{number = 3, name = (null)}
2020-10-12 21:40:53.886793+0800 001---函数与队列[10182:3358751] 里面 a=2--<NSThread: 0x600001b6cb80>{number = 6, name = (null)}
2020-10-12 21:40:53.886919+0800 001---函数与队列[10182:3358751] 里面 a=4--<NSThread: 0x600001b6cb80>{number = 6, name = (null)}
2020-10-12 21:40:53.886983+0800 001---函数与队列[10182:3358757] 里面 a=4--<NSThread: 0x600001b696c0>{number = 3, name = (null)}
2020-10-12 21:40:53.887066+0800 001---函数与队列[10182:3358751] 里面 a=5--<NSThread: 0x600001b6cb80>{number = 6, name = (null)}
2020-10-12 21:40:53.887096+0800 001---函数与队列[10182:3358757] 里面 a=6--<NSThread: 0x600001b696c0>{number = 3, name = (null)}
2020-10-12 21:40:53.887213+0800 001---函数与队列[10182:3358751] 里面 a=7--<NSThread: 0x600001b6cb80>{number = 6, name = (null)}
2020-10-12 21:40:53.889660+0800 001---函数与队列[10182:3358757] 里面 a=8--<NSThread: 0x600001b696c0>{number = 3, name = (null)}
2020-10-12 21:40:53.890100+0800 001---函数与队列[10182:3358751] 里面 a=9--<NSThread: 0x600001b6cb80>{number = 6, name = (null)}
2020-10-12 21:40:53.890302+0800 001---函数与队列[10182:3358601] 外面 a=10

栅栏函数 dispatch_barrier_sync/dispatch_barrier_async

当我们有两组异步任务需要有先后顺序的时候,可以使用栅栏函数解决。有两个api方法,dispatch_barrier_sync和dispatch_barrier_async,等到dispatch_barrier_async或dispatch_barrier_sync方法前的任务执行完毕后才会去执行后边追加到队列中的任务,简单来说dispatch_barrier_async或dispatch_barrier_sync将异步任务分成了两个组,执行完第一组后,再执行自己,然后执行队列中剩余的任务。唯一不同的是dispatch_barrier_async不会阻塞线程。具体使用如下:

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"----------------------%@--------",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加载那么多,喘口气");
    });
    NSLog(@"********起来干");

打印结果

2020-10-12 22:00:53.824550+0800 004--GCD进阶使用[10306:3369587] 123
2020-10-12 22:00:53.825133+0800 004--GCD进阶使用[10306:3369518] ----------------------<NSThread: 0x600003e01cc0>{number = 1, name = main}--------
2020-10-12 22:00:53.825448+0800 004--GCD进阶使用[10306:3369518] ********起来干
2020-10-12 22:00:53.825465+0800 004--GCD进阶使用[10306:3369587] 加载那么多,喘口气

如果换成这个方法dispatch_barrier_async,具体代码如下

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"----------------------%@--------",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加载那么多,喘口气");
    });
    NSLog(@"********起来干");

打印结果为:

2020-10-12 22:09:36.613491+0800 004--GCD进阶使用[10336:3374328] ********起来干
2020-10-12 22:09:37.613944+0800 004--GCD进阶使用[10336:3374484] 123
2020-10-12 22:09:37.614273+0800 004--GCD进阶使用[10336:3374484] ----------------------<NSThread: 0x600001be2780>{number = 7, name = (null)}--------
2020-10-12 22:09:37.614427+0800 004--GCD进阶使用[10336:3374484] 加载那么多,喘口气

注意:
栅栏函数只能控制同一并发队列。所以在网络请求AFN中使用栅栏不起作用
栅栏函数其实是作为堵塞队列而存在的(同步函数是作为堵塞线程存在的)
栅栏函数不能用于全局并发队列,只能用于自定义的并发队列,因为全局队列系统也在用,假如用栅栏函数将全局并发队列堵塞住了,会导致系统运行不正常而导致崩溃。

可变数组 栅栏函数解决线程不安全

看下面的例子:

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

    // signal -- 线程BUG
    for (int i = 0; i<5000; 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];
        });
    }

这里创建这么多线程同时对可变数据操作,造成线程不安全,容易造成崩溃,可以使用栅栏函数解决。

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

    // signal -- 线程BUG
    for (int i = 0; i<5000; 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_t

dispatch_group_t 在日常开发中最直接的作用是控制任务执行顺序。

调度组的使用

dispatch_group_create 创建组
dispatch_group_async 进组任务
dispatch_group_notify 进组任务执行完毕通知
dispatch_group_wait 进组任务执行等待时间
dispatch_group_enter 进组
dispatch_group_leave 出组
dispatch_group_enter 和 dispatch_group_leave必须成对使用

dispatch_group_async

通过以下代码可以发现,无论任务一和任务儿耗时多少,都会在全部执行结束后,调用dispatch_group_notify方法,我们一般在此方法中获取到dispatch_get_main_queue主线程,用来刷新UI等操作。

//创建调度组
        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.group", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_group_async(group, queue, ^{
            sleep(2);
            NSLog(@"任务一");
        });
        
        dispatch_group_async(group, queue1, ^{
            
            NSLog(@"任务二");
        });
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"任务执行完成");
        });

打印结果

2020-10-13 22:12:22.163405+0800 005---GCD进阶使用(下)[10816:3415753] 任务二
2020-10-13 22:12:24.166883+0800 005---GCD进阶使用(下)[10816:3415749] 任务一
2020-10-13 22:12:24.167300+0800 005---GCD进阶使用(下)[10816:3415694] 任务执行完成

dispatch_group_enter 和 dispatch_group_leave

使用进出组的方式和dispatch_group_async效果相同,只不过是有了进出组的代码,逻辑更加清晰明了。而且dispatch_group_enter和dispatch_group_leave是成对使用的,必须先进组再出组,缺一不可。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"第一个走完了");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"第二个走完了");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任务完成,可以更新UI");
    });

打印结果

2020-10-13 22:21:13.499500+0800 005---GCD进阶使用(下)[10858:3421125] 第二个走完了
2020-10-13 22:21:15.504329+0800 005---GCD进阶使用(下)[10858:3421124] 第一个走完了
2020-10-13 22:21:15.504693+0800 005---GCD进阶使用(下)[10858:3421072] 所有任务完成,可以更新UI

dispatch_group_wait 设置等待时间,区分异步任务执行

dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout) 需要传入两个参数,dispatch_time_t timeout:参数用来指定等待的时间。
这里的等待表示,一旦调用dispatch_group_wait函数,该函数就处理调用的状态而不返回值,只有当函数的currentThread停止,或到达wait函数指定的等待的时间,或Dispatch Group中的操作全部执行完毕之前,执行该函数的线程停止.
当指定timeout为DISPATCH_TIME_FOREVER时就意味着永久等待
当指定timeout为DISPATCH_TIME_NOW时就意味不用任何等待即可判定属于Dispatch Group的处理是否全部执行结束
如果dispatch_group_wait函数返回值不为0,就意味着虽然经过了指定的时间,但Dispatch Group中的操作并未全部执行完毕
如果dispatch_group_wait函数返回值为0,就意味着Dispatch Group中的操作全部执行完毕。
这里的应用场景可以在任务一和任务二两个异步任务,也要有先后的执行顺序时,通过wait来阻塞当前线程,只有当执行完一组任务或者超过超时时间后才可以继续向下进行。
通过一个例子来看一下。通过打印结果可以发现,当执行的之间为DISPATCH_TIME_FOREVER或者未超时时,是先执行了任务一后,才执行的任务二,然后回到主线程的。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_group_t group = dispatch_group_create();
        
        dispatch_group_enter(group);
        
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"任务一");
            dispatch_group_leave(group);
        });
        
    //    long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,3 * NSEC_PER_SEC));
        long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_FOREVER, 0));
        if (timeout == 0) {
            NSLog(@"回来了");
        }else{
            NSLog(@"等待中 -- 转菊花");
        }
        
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"任务二");
            dispatch_group_leave(group);
        });
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"所有任务完成,可以更新UI");
        });

打印结果

2020-10-13 22:27:13.708640+0800 005---GCD进阶使用(下)[10875:3424596] 任务一
2020-10-13 22:27:13.709019+0800 005---GCD进阶使用(下)[10875:3424432] 回来了
2020-10-13 22:27:13.709438+0800 005---GCD进阶使用(下)[10875:3424596] 任务二
2020-10-13 22:27:13.735163+0800 005---GCD进阶使用(下)[10875:3424432] 所有任务完成,可以更新UI

这里修改一下等待时间,改为long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC));打印结果为

2020-10-13 22:30:15.981184+0800 005---GCD进阶使用(下)[10894:3426454] 等待中 -- 转菊花
2020-10-13 22:30:15.981576+0800 005---GCD进阶使用(下)[10894:3426516] 任务二
2020-10-13 22:30:16.981933+0800 005---GCD进阶使用(下)[10894:3426518] 任务一
2020-10-13 22:30:16.982324+0800 005---GCD进阶使用(下)[10894:3426454] 所有任务完成,可以更新UI

dispatch_source_t

dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件,在日常的开发中,我们经常使用它来创建一个GCDTimer。但是它还有很多其他的监听类型,通过查看官方文档我们可得以下监听:

  • Timer Dispatch Source:定时调度源。
  • Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
  • Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
  • Process Dispatch Source:监听进程相关状态的调度源。
  • Mach port Dispatch Source:监听Mach相关事件的调度源。
  • Custom Dispatch Source:监听自定义事件的调度源

主要的API如下:
dispatch_source_create: 创建事件源
dispatch_source_set_event_handler: 设置数据源回调
dispatch_source_merge_data: 设置事件源数据
dispatch_source_get_data: 获取事件源数据
dispatch_resume: 继续
dispatch_suspend: 挂起
dispatch_cancle: 取消

GCD定时器

在iOS开发中使用定时器一般会用NSTimer,但NSTimer是依赖Runloop的,而Runloop可以运行在不同的模式下,如果NSTimer添加在一种模式下,当Runloop运行在其他模式下的时候,定时器就“不走”了,因此NSTimer在计时上会有误差,并不是特别精确,而GCD定时器不依赖Runloop,计时精度要高很多。

@interface GCDTimer : NSObject

// 开始
- (void)start;
// 暂停
- (void)supend;
// 继续
- (void)Continue;

- (instancetype)initWithTimeInterval:(NSInteger) TimeInterval block:(void (^)(NSInteger timerNum))block ;
 
@end
@interface GCDTimer()

@property (nonatomic,assign)NSInteger timeInterval;
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, copy) void(^block)(NSInteger timerNum);
@property (nonatomic) BOOL isRunning;
@end

@implementation GCDTimer

- (instancetype)initWithTimeInterval:(NSInteger) TimeInterval block:(void (^)(NSInteger timerNum))block{
    if (self == [super init]) {
        
        self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        self.queue = dispatch_queue_create("com.tz.cn.cooci", 0);
        self.isRunning = YES;
        self.timeInterval = TimeInterval;
        self.block = block;
        
        // 保存代码块 ---> 异步 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.timeInterval -= 1;
            if ( self.timeInterval == 0) {
                [self supend];
            }
            block(self.timeInterval);
        });
        
        dispatch_resume(self.source);
        
        dispatch_source_set_cancel_handler(self.source, ^{
            NSLog(@"---- %d",__LINE__);
        });
    }
    return self;
}


// 开始
- (void)start{
    dispatch_async(self.queue, ^{
        if (!self.isRunning) {
            NSLog(@"暂停下载");
            return ;
        }
        dispatch_source_set_timer(self.source, DISPATCH_TIME_NOW, 1.0*NSEC_PER_SEC, 0);
    });
}

// 继续
- (void)Continue{
    dispatch_resume(self.source);
    dispatch_resume(self.queue);
    self.isRunning = YES;
}

// 暂停
- (void)supend{
    dispatch_suspend(self.source);
    dispatch_suspend(self.queue);// mainqueue 挂起
    self.isRunning = NO;
    
}

@end

调用

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *button;
@property (strong, nonatomic)GCDTimer *gcdTimer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    GCDTimer *gcdTimer = [[GCDTimer alloc]initWithTimeInterval:60 block:^(NSInteger timerNum) {
        [self.button setTitle:[NSString stringWithFormat:@"倒计时:%ld 秒",timerNum] forState:UIControlStateNormal];
    }];
    self.gcdTimer = gcdTimer;
    
//    [BYTimer timerWithTimeInterval:60 block:^(BYTimer *timer) {
//
//    }];
}

// 开始
- (IBAction)beginBtnClick:(UIButton *)sender {
    
    [self.gcdTimer start];
}

// 暂停
- (IBAction)supendClick:(UIButton *)sender {
      [self.gcdTimer supend];
}

// 继续
- (IBAction)continueClick:(UIButton *)sender {
         [self.gcdTimer Continue];
}

相关文章

网友评论

      本文标题:iOS 底层原理 - GCD应用

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