iOS实录13:GCD使用小结

作者: 南华coder | 来源:发表于2017-06-29 18:04 被阅读993次

    [这是第13篇]

    导语:在iOS中,多线程方案有四种:pthread、NSThread、NSOperation & NSOperationQueue 和 GCD,但是开发中GCD使用得最多,本文主要总结一下我使用GCD的情况。

    一、GCD(Grand Central Dispatch)概述

    1、基本概念
    • GCD允许程序将任务切分为多个单一任务提交至Dispatch Queue,然后系统调度线程,实现并发或者串行地执行任务。GCD隐藏了内部线程的调度,开发者只需要关注创建或获取队列,然后将Block追加到队列中即可。

    • 在iOS中有两种队列,分别是串行队列并发队列

    • 串行队列:同一时间队列中只有一个任务在执行,每个任务只有在前一个任务执行完成后才能开始执行。主队列(通过dispatch_get_main_queue()获取,提交至主队列的任务会在主线程中执行) 就是串行队列,也可以使用dispatch_queue_create创建串行队列。

    串行队列.png
    • 并发队列:这些任务会按照被添加的顺序开始执行。但是任意时刻有多个Block(任务)运行,这个完全是取决于GCD。并发队列可以使用dispatch_queue_create创建,也可以获取进程中的全局队列,全局队列有:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入相应优先级来访问队列。
    并发队列.png
    • 同步执行:阻塞当前线程,直到当前block中任务执行完毕才返回。同步并不创建新线程。不能使用sync将任务添加到主队列,这样会造成死锁。

    • 异步执行:不会阻塞当前线程,函数会立即返回, block会在后台异步执行;异步必定会开启新线程。

    说明1:有些博客中将并发队列说成 并行队列,这是不对的。因为并行是多个事件在同一时刻发生,而并发是多个事件在同一时间间隔发生;并行完全依赖处理器的核数。而并发才能充分的利用处理器的每一个核,以达到最高的处理性能。

    说明2队列不等于线程。 作为开发者的我们,只是将Block添加进合适的GCD队列,真正的线程的调度是由系统完成的;无论同步(sync)还是异步(async)向主队列提交Block,最终Block都是在主线程中执行;同步(sync)往非主队列中提交Block,会在当前线程中执行; 如果是异步(async)往非主队列中提交Block,则会在分线程中执行。

    2、串行队列/并发队列 和 同步/异步的组合 ( 重点 )#####

    有四种组合方式

    ** 1)串行队列 + 同步组合(常用)**

    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
    //    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", NULL);
    //
    dispatch_sync(serialQueue, ^{
        NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);
    });
    
    dispatch_sync(serialQueue, ^{
        NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);
    });
    
    dispatch_sync(serialQueue, ^{
        NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);
    });
    
    串行队列 + 同步组合结果.png

    说明1:串行队列 (自己创建的串行线程)+ 同步组合下,不会新建线程,依然在当前线程上执行任务。不可以在主线程中使用sync方法,会造成死锁。

    说明2:比较常用,同步锁的替代方法。

    ** 2)串行队列 + 异步组合**

    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
    //    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", NULL);
    //
    dispatch_async(serialQueue, ^{
        NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]);
    });
    
    dispatch_async(serialQueue, ^{
        NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]);
    });
    
    dispatch_async(serialQueue, ^{
        NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]);
    });
    
    串行队列 + 异步组合结果.png

    说明:串行队列(无论是自己创建的,还是获取主队列) + 异步组合下,会新建线程,但只开启一条线程;

    ** 3)并发队列 + 同步组合**

    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"并发队列 + 同步1:%@",[NSThread currentThread]);
    });
    
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"并发队列 + 同步2:%@",[NSThread currentThread]);
    });
    
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"并发队列 + 同步3:%@",[NSThread currentThread]);
    });
    
    并发队列 + 同步组合结果.png

    说明: 并发队列(无论是自己创建的,还是获取全局队列) + 同步组合下,并没有新建线程,任务依然在当前线程上执行。

    ** 4)并发队列 + 异步组合(常用)**

    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);
    });
    
    并发队列 + 异步组合结果.png

    说明:并发队列(无论是自己创建的,还是获取全局队列) + 异步组合下,会新建线程,iOS 系统中可以开多条线程。

    | |同步 | 异步|
    |--|--|
    |串行队列| 1、不会新建线程,依然在当前线程上执行任务;</br>2、类似同步锁,是同步锁的替代方案| 1、会新建线程,但只开启一条线程;</br>2、每次使用 dispatch_queue_create创建串行队列,就会创建一条新线程;多次创建,会创建多条线程,多条线程间并发执行。|
    |并发队列| 不会新建线程,依然在当前线程上执行任务| 1、会新建线程,可以开多条线程;</br>2、iOS7-SDK 时代一般是5、6条, iOS8-SDK 以后可以50、60条|

    总结1:不可以在主线程中使用sync方法,否则会造成死锁。

    总结2:串行队列 + 同步组合 可以替代同步锁;

    总结3:为了提高效率,如多线程下载图片等,并发队列 + 异步比较常用。

    二、GCD使用1:异步处理

    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
        // 一个异步的任务,如网络请求,耗时的文件操作等等
        ...
        dispatch_async(dispatch_get_main_queue(), ^{
            // UI刷新 或其他主线程操作
            ...
        });
    });
    

    说明:该用法最常见,如开启一个异步的网络请求,待数据返回后在主线程刷新UI等。

    三、GCD使用2:单例

    dispatch_once实现单例,以实现QSAccountManager单例为例。源码如下:

    1、实现

    //QSAccountManager.m
    @implementation QSAccountManager
    
    static QSAccountManager *_shareManager = nil;
    + (instancetype)shareManager{
    
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            _shareManager = [[self alloc] init];
        });
        return _shareManager;
    }
    
    + (instancetype)allocWithZone:(struct _NSZone *)zone{
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _shareManager = [super allocWithZone:zone];
        });
        return _shareManager;
    }
    
    - (nonnull id)copyWithZone:(nullable NSZone *)zone{
        return _shareManager;
    }
    
    @end
    

    说明1:dispatch_once函数中,参数1是代码块是否被调用的谓词,参数2是被调用的代码块。该函数中的代码块只会被执行一次,而且还是线程安全的。

    说明2:要保证单例类只有一个唯一的实例,还需要实现allocWithZone和copyWithZone方法,这保证使用init和copy方法返回也是唯一实例。

    2、使用

    QSAccountManager *account1 = [QSAccountManager shareManager];
    QSAccountManager *account2 = [QSAccountManager new];
    QSAccountManager *account3 = [[QSAccountManager alloc]init];
    QSAccountManager *account4 = [account3 copy];
    
    NSLog(@"account1 = %@",account1);
    NSLog(@"account2 = %@",account2);
    NSLog(@"account3 = %@",account3);
    NSLog(@"account4 = %@",account4);
    
    单例输出结果.png

    四、GCD使用3:代替同步锁

    • atomic 的内存管理语义是原子性的,仅保证了属性的setter和getter方法是原子性的,但是执行效率低,可以使用GCD实现。

    • @synchronized(self)同步块机制,会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕,才释放锁。执行效率低。

    • 替代方案:将数据的读取和写放入串行同步队列,保证数据同步,线程安全。

    • 替代方案:结合GCD的栅栏块(barrier)和 并发队列 实现数据同步,线程安全。(比串行同步队列方式更高效)

    1、代替atomic实现线程安全的setter和getter方法
    //串行队列
    _syncQueue = dispatch_queue_create("com.jzp.syncQueue",NULL);
    
    //假设属性是someString
    - (NSString *)someString {
        __block NSString *localSomeString;
        dispatch_sync(_syncQueue, ^{
            localSomeString = _someString;
        });
        return localSomeString;
    }
    
    - (void)setSomeString:(NSString *)someString {
        dispatch_sync(_syncQueue, ^{
            _someString = someString;
        });
    }
    
    2、实现线程安全的NSMutableArray
    • 主要依靠栅栏块单独执行的特性,在并发队列中如果发现接下来要处理的块是个栏栅块,那么就一直等到当前所有并发块都执行完毕,才会单独执行这个栏栅块。待栏栅块执行过后,再按正常方式继续向下处理。

    • 这部分实现详见 iOS实录12:NSMutableArray使用中忽视的问题“一、线程安全的NSMutableArray”

    说明:dispatch_barrier_sync和dispatch_barrier_async只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。

    五、GCD使用4:dispatch_group实现线程同步

    1、简单模式#####
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        // 任务1
    
    });
    
    dispatch_group_async(group, queue, ^{
        // 任务2
    });
    
    // 等待group中多个异步任务执行完毕,会发出同步信号
    
    // 方式1(会阻塞当前线程,group上任务都完成或超时等待就执行)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    // ...
    
    // 方式2(不会阻塞当前线程,group上任务都完成,执行block中代码)
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 任务完成后,在主队列中做一些操作
      
    });
    

    说明:将block(任务)放入队列中执行,并和调度组 group相关联;如果提交到dispatch queue中的block全都执行完毕,会执行dispatch_group_notify中的block代码; 或在group上任务完成前,dispatch_group_wait会阻塞当前线程(所以不能放在主线程调用)一直等待;当group上任务完成,或者等待时间超过设置的超时时间会结束等待。

    2、多异步任务的同步#####
    • 成对使用dispatch_group_enterdispatch_group_leave,可以将异步任务加入group中;

    • 当这些异步任务处理完成后,dispatch_group_notify和dispatch_group_wait会收到同步信号;

    • 异步任务如请求,通过该机制实现批量请求的处理。

    dispatch_group_t batch_request_group = dispatch_group_create();
    
    dispatch_group_enter(batch_request_group);
    [self.request1 startWithCompleteBlock:^(BOOL isSuccess, id  _Nullable responseObj, NSString * _Nonnull errorDesc) {
        
        //TODO 数据解析....
        
         dispatch_group_leave(batch_request_group);
    }];
    
    dispatch_group_enter(batch_request_group);
    [self.request2 startWithCompleteBlock:^(BOOL isSuccess, id  _Nullable responseObj, NSString * _Nonnull errorDesc) {
        
        //TODO 数据解析....
        
        dispatch_group_leave(batch_request_group);
    }];
    
    dispatch_group_enter(batch_request_group);
    [self.request3 startWithCompleteBlock:^(BOOL isSuccess, id  _Nullable responseObj, NSString * _Nonnull errorDesc) {
        
        //TODO 数据解析....
        
        dispatch_group_leave(batch_request_group);
    }];
    
    dispatch_group_notify(batch_request_group, dispatch_get_main_queue(), ^{
        
        //三个请求都结束了,继续处理
    });
    

    六、GCD使用其他

    1、dispatch_apply#####

    按 指定的次数 将指定的Block追加到 指定的Dispatch Queue中, 并等到全部处理执行结束。有并行的运行机制,效率一般快于for循环的类串行机制。

    /**
     @param 10 指定重复次数,这里指定10次
     @param gQueue 追加对象的Dispatch Queue
     @param index 带有参数的Block, index的作用是为了按执行的顺序区分各个Block
     */
    dispatch_apply(10, gQueue, ^(size_t index) {
       
        NSLog(@"%zu",index);
        
    });
    
    2、dispatch_after#####

    延迟执行

    // NSEC_PER_SEC,每秒有多少纳秒。
    // USEC_PER_SEC,每秒有多少毫秒。
    // NSEC_PER_USEC,每毫秒有多少纳秒。
    // DISPATCH_TIME_NOW 从现在开始
    // DISPATCH_TIME_FOREVE 永久
    
    // time为5s
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5.0 * NSEC_PER_SEC));
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_after(time, queue, ^{
        // 在queue里面延迟执行的一段代码
        // ...
        
    });
    
    3、小心死锁#####
    • 死锁情况1: 在主线程中使用sync方法
    dispatch_sync(dispatch_get_main_queue(), ^{
        // 任务
        ...
    });
    
    • **死锁情况2: ** 在串行队列添加同步任务;
    // 在串行队列添加同步任务 
    dispatch_sync(serialQueue, ^{
        // 任务
        dispatch_sync(serialQueue, ^{
            // 任务
        });
    };
    

    End

    相关文章

      网友评论

      • EmptyWalker:博主:对于这段话 “dispatch_barrier_sync和dispatch_barrier_async只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样”。 除了代码验证,有没有原理解释???
        EmptyWalker:@南华coder 明白了。thks
        南华coder:dispatch_barrier_sync 和 dispatch_barrier_async提交的任务,会在执行时会阻塞队列中后面的任务,全局队列是所有的应用程序都可以用的队列;假设app A的任务通过dispatch_barrier提交到全局队列,阻塞全局队列中其他任务(app A其他任务或app B的任务),这对GCD处理任务调度,很棘手,干脆等效dispatch_(a)sync。
      • 5f899fd07a6f:还有穿行队列的那个图,使用一个线程,这句话有歧义,横向来看,确实穿行队列只在某个时间点使用一个线程,但是如果纵向来看,在某个时间段内,可能使用几个不同的线程
        9e72b5a52cd1:@wszcug 不好意思啊!打扰一下,您说纵向看,这些任务可能是由不同的线程执行的,这句话该如何理解呢?谢谢
        5f899fd07a6f:@南华coder 不是,给一个串行队列提交n个不同的任务,横向来看,这个队列只有一个线程在跑,但是纵向看,这些任务可能是由不同的线程执行的
        南华coder:这个我坚持自己看法。一个任务放在串行队列中,系统只会分配一个线程去处理。而在某个时间段内,可能使用几个不同的线程,那是若干个串行线程之间的并发处理吧。
      • 5f899fd07a6f:我觉得,理解的有些问题
        不能使用sync将任务添加到主队列,这样会造成死锁。 这句话不对。。。。。sync 完全可以将任务添加到主队列,条件是只要 sync 所在队列不是主队列即可。。
        南华coder:@wszcug 谢谢提醒,是我表述不对,已经fix

      本文标题:iOS实录13:GCD使用小结

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