GCD学习

作者: 勇往直前888 | 来源:发表于2017-03-09 20:42 被阅读24次

    多线程技术,首选的是NSOperation,在有些场合,考虑直接用GCD。c语言的风格,block的调用方式,更加灵活可控。
    由于苹果推荐说用GCD,不过这是c风格了,跟Object-C这种面向对象的风格不是很符合。还是推荐NSOperation为主。当然,方便的时候,用用简单的经典的GCD也无妨。AFNetworking就是将NSOperationGCD结合起来用的,比较经典。
    GCD中一些类型的定义在系统的如下路径
    /usr/include/dispatch/

    下面这两篇文章比较好地解释了同步异步,串行并行的概念,可以参考一下。虽然文章名字差不多,不过真的是不一样的,而且写得都还不错。
    iOS多线程与GCD 你看我就够了
    关于iOS多线程,你看我就够了

    同步/异步(执行)

    差异点在于是否阻塞当前的调用者线程,等待队列中的任务执行完毕。

    同步执行

    typedef void (^dispatch_block_t)(void);
    
    dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
    
    • “阻塞”当前的调用者线程
    • “等待”队列中的任务执行完毕
    • 激活当前的调用者线程,“然后”继续往下执行其他代码
    • 如果队列中的任务长时间不返回,会出现界面“卡死”现象
    • 如果调用者线程和队列执行线程是“同一线程”,会出现“自己等自己”现象,结果是“崩溃或者死机”
      比如下面的代码如果在主线程执行,就会“崩溃或者死机”,原因就是出现了“主线程等主线程自己的情况”
    NSLog(@"之前 - %@", [NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"sync - %@", [NSThread currentThread]);
    });
    NSLog(@"之后 - %@", [NSThread currentThread]);
    

    异步执行

    dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    

    格式基本和同步的差不多,就一个单词的差别,但是表现完全两样。上面的例子用这个函数的话,就不会“崩溃或者死机”。其输出一般情况是这样的:

    之前 - 
    之后 - 
    sync - 
    
    • 只是派发队列中的函数,让其去执行,不阻塞自己,不等待
    • 派发完后,直接执行下面的语句,碰到},自己就先返回
    • 队列中的函数如何执行,在哪执行,当前的调用者线程完全不关心
    • 异步调用可以使“当前的调用者”和“队列如何执行”毫无关系,充分隔离
    • 如果当前做的事和队列中要做的事没有先后依赖关系,那么推荐用异步调用的方式

    串行/并行(队列)

    dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
    

    第一个参数是队列的名字,是个字符串;
    第二个参数决定队列的性质,串行或者并行;
    虽然有ARC,但生成的Dispatch Queue必须由程序员主动释放。

    dispatch_release(exampleSerialDispatchQueue);  // 释放
    dispatch_retain(exampleSerialDispatchQueue);   // 持有
    
    • 两者的本质区别是:后面的任务是否等待前面的任务执行完毕

    串行队列

    • 第二个参数是DISPATCH_QUEUE_SERIAL或者是"NULL"
    • 任务FIFO;因为只有一个管道,所以任务之间间有顺序依赖。就算是异步执行的,也要等前面的任务执行完毕,后面的任务才能执行
    • 队列的优先级较高,串行队列中的任务,有先后顺序依赖,后面的任务要等待
    • 如果任务间有顺序依赖,可以用串行队列,不需要考虑进程间同步的问题。“线程安全的”就是指串行队列。比如,可以把字典、数组操作放入一个串行队列,就解决“资源竞争”的问题了。
    • 只开辟一个线程执行,一个执行完了,再接着下一个,所以没有时序问题,是“线程安全的”
    • 主线程是一种串行队列,有时候主线程“卡死”,就是因为它在等待“耗时的任务”执行完毕
    ispatch_queue_t dispatch_get_main_queue(void) {
        return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);
    }
    

    并行队列

    • 第二个参数是DISPATCH_QUEUE_CONCURRENT
    • 任务也是FIFO。但是后面的任务不需要等待前面的任务执行结束,没有先后的依赖关系。
    • 后面的任务要后出队列,调度顺序上要落后一点。但是由于任务时间的关系。后面的任务完全有可能比前面的任务的先执行完毕
    • 并行队列不需要等待前面的任务执行完毕,效率上会高一点。如果任务间没有顺序上的依赖,推荐用并行队列
    • 如果是异步执行,可以开辟多个线程,任务间时序是不确定的。当然,如果是同步执行,任务间还是得等待,有先后依赖关系。
    • 系统提供的默认可用的并行队列
    dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
    
    #define DISPATCH_QUEUE_PRIORITY_HIGH 2
    #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    #define DISPATCH_QUEUE_PRIORITY_LOW (-2)
    #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
    

    第一个参数表示队列的优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT;第2个参数基本上是0

    单例

    + (instancetype)sharedManager {
        static AFNetworkReachabilityManager *_sharedManager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            struct sockaddr_in address;
            bzero(&address, sizeof(address));
            address.sin_len = sizeof(address);
            address.sin_family = AF_INET;
    
            _sharedManager = [self managerForAddress:&address];
        });
    
        return _sharedManager;
    }
    
    • 这是AFNetworking中一个例子,单例基本就这个格式,非常方便
    • static可以是全局的,也可以是局部的。这里是局部的,其他文件访问不到,更好一点
    • typedef long dispatch_once_t; 本质是个long,这里有特殊含义,特殊用途

    延时执行

    void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
    dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
    
    typedef uint64_t dispatch_time_t;
    #define DISPATCH_TIME_NOW (0ull)
    #define DISPATCH_TIME_FOREVER (~0ull)
    

    上面就是常用的函数和相应的配套函数和参数。第1个参数是时间,一般用第2个函数来获得,名字看上去都差不多。最小单位是纳秒,所以一般时间要用到下面几个常数定义。这个延时比NSTimer要精确一点,很多第3方库都在用,可以作为经典形式。
    下面就是YYCache中的一个实际的例子

    - (void)_trimRecursively {
        __weak typeof(self) _self = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
            __strong typeof(_self) self = _self;
            if (!self) return;
            [self _trimInBackground];
            [self _trimRecursively];
        });
    }
    

    _autoTrimInterval就是要延迟执行的秒数

    Group操作

    有时候我们会有这种需求,在刚进去一个页面需要发送两个请求,并且更新页面操作必须在两个请求都结束(成功或失败)的时候才会执行。
    第1种方式是用串行队列,两个网络请求依次进行,然后更新界面。这样做时序没有问题,不过当这两个网络请求之间没有相互依赖的话,效率损失比较大。
    第2种方式是用NSOperationQueue,将最后的更新页面操作依赖前面2个网络操作就可以了,其他的事交给系统。这种是推荐的方式
    第3种是用GCDdispatch_group函数族,可以获得更高的性能,更灵活地控制。这种方式不推荐,不过在AFNetworking中有比较大量的应用,所以也不反对使用。

    dispatch_group_t dispatch_group_create(void);
    void dispatch_group_enter(dispatch_group_t group);
    void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    void dispatch_group_leave(dispatch_group_t group);
    

    dispatch_group_wait比较特殊,正常情况下不需要用到。比如,在这个例子中,两个网络请求之后就更新界面了。但是如果网络请求因为某种原因长时间不返回呢(这种情况还是蛮多的)?那就设一个超时时间吧。比如,最多等30s,如果还不返回,就显示网络不给力了。这个函数是同步的,傻等,结果看返回值,0表示成功。常见的超时等待处理方式。这种能力NSOperationQueue是没有的。

    代码参考格式:

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_async(dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //在这里执行异步请求A
        并且在执行结束代码(成功或失败)中写上dispatch_group_leave(group);
    });
    dispatch_async(dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //在这里执行异步请求B
        并且在执行结束代码(成功或失败)中写上dispatch_group_leave(group);
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //在这里执行异步请求B
    });
    

    NSOperationQueue复杂不了多少。

    关于超时的参考格式:

    #define kTimeOut   30ull 
    
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (kTimeOut * NESC_PER_SEC));
    long result = dispatch_group_wait(group, time);
    if(0 == result) { 
        // 在限定时间内返回;dispatch_group_notify执行,正常情况;啥也不用做,当然也可以log一下
    } else { 
        // 超时了,任务还没有全部完成,dispatch_group_notify这时还没有执行
        // 如果这里显示超时,网络请求并不能取消,这个有点尴尬,dispatch_group_notify还是有可能执行的,只是观众不愿意等了
    };
    

    一些扫尾的工作

    在文件object.h中有一些函数定义:

    void dispatch_suspend(dispatch_object_t object);
    void dispatch_resume(dispatch_object_t object);
    void dispatch_cancel(void *object);
    void dispatch_release(dispatch_object_t object);
    
    • 在上面超时的例子中,是不是可以把group直接dispatch_cancel掉?
    • 在离开页面的时候,是不是要把group dispatch_release
    • 有传说CGD的任务不能cancel,那是因为block是匿名函数,没法获得地址。如果把block用个变量保存起来,不是可以cancel了吗?或者直接cancel或者suspend block所在的queue,不是能取消了吗?

    栅栏函数(这部分内容有点多,Option)

    // 异步栅栏,比较常用
    void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    
    // 同步栅栏,会阻塞当前的调用者线程,
    void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
    

    栅栏函数可以看做是dispatch_group的升级版。就像一个分界点,将队列一分为二,前后两部分有顺序依赖。可以简单的认为:

    1. 执行栅栏函数之前加入队列的任务;等待,直到任务全部执行完毕
    2. 执行栅栏函数添加到队列中的任务;
    3. 等待栅栏函数添加的任务,直到任务全部执行完毕。
    4. 执行栅栏函数之后添加的任务

    下面这篇文章写得很不错,值得好好看看。以前一直讨厌这个栅栏,这次算有点理解了,开始喜欢这个栅栏函数:
    通过GCD中的dispatch_barrier_(a)sync加强对sync中所谓等待的理解

    实际的例子

    - (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
        dispatch_barrier_async(_queue, ^{
            [_array insertObject:anObject atIndex:index];
        });
    }
    
    • 这是weex框架中的一个例子
    • 在类WXThreadSafeMutableArray
    • 这里可以当做dispatch_group的简化使用,因为栅栏之后没内容,作用跟dispatch_group一样,不过使用简单很多,是一种很讨巧的方式
    • 鼓励这种用法,相比而言,dispatch_group实在有点复杂

    自己写的一个例子

    • 任务用简单的sleep表示
    • 先加入的任务时间长(1秒),后加入的任务时间短(0.5秒),在同步的情况下是顺序的,在异步的情况下是倒序的,容易区分
    • 调用者线程就是主线程,好理解一点
    case1: 异步栅栏在前,同步栅栏在后

    这种情况,将有可能影响当前线程(主线程)的同步操作放在后面,对当前线程的影响最小,相对好理解一点。

    代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        dispatch_queue_t currentQueue = dispatch_queue_create("currentQueue", DISPATCH_QUEUE_CONCURRENT);
        // 异步执行
        dispatch_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"dispatch_async 1");
        });
        dispatch_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_async 2");
        });
        
        // 栅栏异步执行
        dispatch_barrier_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_barrier_async");
        });
        NSLog(@"main thread after dispatch_barrier_async");
        
        // 栅栏同步执行
        dispatch_barrier_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_barrier_sync");
        });
        NSLog(@"main thread after dispatch_barrier_sync");
        
        // 同步执行
        dispatch_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"dispatch_sync 3");
        });
        dispatch_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_sync 4");
        });
    }
    

    输出的log:

    2017-03-09 17:09:25.573 Barrier[82252:5039848] main thread after dispatch_barrier_async
    2017-03-09 17:09:26.141 Barrier[82252:5039953] dispatch_async 2
    2017-03-09 17:09:26.639 Barrier[82252:5039954] dispatch_async 1
    2017-03-09 17:09:26.639 Barrier[82252:5039954] dispatch_barrier_async
    2017-03-09 17:09:26.640 Barrier[82252:5039848] dispatch_barrier_sync
    2017-03-09 17:09:26.640 Barrier[82252:5039848] main thread after dispatch_barrier_sync
    2017-03-09 17:09:27.715 Barrier[82252:5039848] dispatch_sync 3
    2017-03-09 17:09:28.289 Barrier[82252:5039848] dispatch_sync 4
    

    过程分析:

    1. main thread执行到dispatch_barrier_sync, 被阻塞,等待。这个时候,main thread after dispatch_barrier_async已经输出
    2. 任务1和2异步执行,逆序输出(2的任务时间短)
    3. 异步栅栏执行,输出dispatch_barrier_async
    4. 同步栅栏执行,输出dispatch_barrier_sync
    5. main thread被唤醒,继续执行,输出main thread after dispatch_barrier_sync
    6. main thread执行到dispatch_sync 3, 被阻塞,等待。
    7. 任务3,4同步执行,顺序输出(4的任务时间短也没用,要等3完成才能执行)
    8. main thread被唤醒,继续执行,碰到}返回,过程结束
    case2: 同步栅栏在前,异步栅栏在后

    这里主要看栅栏同步异步的影响

    代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        dispatch_queue_t currentQueue = dispatch_queue_create("currentQueue", DISPATCH_QUEUE_CONCURRENT);
        // 异步执行
        dispatch_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"dispatch_async 1");
        });
        dispatch_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_async 2");
        });
        
        // 栅栏同步执行
        dispatch_barrier_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_barrier_sync");
        });
        NSLog(@"main thread after dispatch_barrier_sync");
        
        // 栅栏异步执行
        dispatch_barrier_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_barrier_async");
        });
        NSLog(@"main thread after dispatch_barrier_async");
        
        // 同步执行
        dispatch_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"dispatch_sync 3");
        });
        dispatch_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_sync 4");
        });
    }
    

    输出的log:

    2017-03-09 20:59:47.526 Barrier[91896:5120619] dispatch_async 2
    2017-03-09 20:59:48.027 Barrier[91896:5120621] dispatch_async 1
    2017-03-09 20:59:48.528 Barrier[91896:5120438] dispatch_barrier_sync
    2017-03-09 20:59:48.528 Barrier[91896:5120438] main thread after dispatch_barrier_sync
    2017-03-09 20:59:48.529 Barrier[91896:5120438] main thread after dispatch_barrier_async
    2017-03-09 20:59:49.029 Barrier[91896:5120621] dispatch_barrier_async
    2017-03-09 20:59:50.051 Barrier[91896:5120438] dispatch_sync 3
    2017-03-09 20:59:50.626 Barrier[91896:5120438] dispatch_sync 4
    

    过程分析:

    1. main thread执行到dispatch_barrier_sync, 被阻塞,等待。这个时候,啥也没有输出
    2. 任务1和2异步执行,逆序输出(2的任务时间短)
    3. 同步栅栏执行,输出dispatch_barrier_sync
    4. main thread执行到dispatch_sync 3, 被阻塞,等待。这段时间main thread做的事情有:(a)main thread after dispatch_barrier_sync输出;(b)异步栅栏被分配,但是main thread没有被阻塞;(c)main thread after dispatch_barrier_async输出;
    5. 异步栅栏执行,输出dispatch_barrier_async
    6. 任务3,4同步执行,顺序输出(4的任务时间短也没用,要等3完成才能执行)
    7. main thread被唤醒,继续执行,碰到}返回,过程结束
    case3: 同步过程在前,异步过程在后

    这个例子,调用者线程(这里是main thread)分配任务之后先于工作者线程返回,是大多数要用到的情况。

    代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        dispatch_queue_t currentQueue = dispatch_queue_create("currentQueue", DISPATCH_QUEUE_CONCURRENT);
        
        // 同步执行
        dispatch_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"dispatch_sync 3");
        });
        dispatch_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_sync 4");
        });
        
        // 栅栏同步执行
        dispatch_barrier_sync(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_barrier_sync");
        });
        NSLog(@"main thread after dispatch_barrier_sync");
        
        // 栅栏异步执行
        dispatch_barrier_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_barrier_async");
        });
        NSLog(@"main thread after dispatch_barrier_async");
        
        // 异步执行
        dispatch_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"dispatch_async 1");
        });
        dispatch_async(currentQueue, ^{
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"dispatch_async 2");
        });
    }
    

    输出的log:

    2017-03-09 21:08:39.715 Barrier[92752:5127802] dispatch_sync 3
    2017-03-09 21:08:40.285 Barrier[92752:5127802] dispatch_sync 4
    2017-03-09 21:08:40.856 Barrier[92752:5127802] dispatch_barrier_sync
    2017-03-09 21:08:40.856 Barrier[92752:5127802] main thread after dispatch_barrier_sync
    2017-03-09 21:08:40.856 Barrier[92752:5127802] main thread after dispatch_barrier_async
    2017-03-09 21:08:41.430 Barrier[92752:5127902] dispatch_barrier_async
    2017-03-09 21:08:41.999 Barrier[92752:5127901] dispatch_async 2
    2017-03-09 21:08:42.499 Barrier[92752:5127902] dispatch_async 1
    

    过程分析:

    1. 任务3,4同步执行,顺序输出(4的任务时间短也没用,要等3完成才能执行)
    2. 同步栅栏执行,输出dispatch_barrier_sync
    3. 这段时间main thread一直被阻塞,直到同步栅栏执行完毕
    4. main thread被唤醒,继续执行,碰到}返回,main thread的过程结束。这段时间main thread做的事情有:(a)main thread after dispatch_barrier_sync输出;(b)异步栅栏被分配,但是main thread没有被阻塞;(c)main thread after dispatch_barrier_async输出;(d)任务1和2被分配,但是main thread没有被阻塞;
    5. 异步栅栏执行,输出dispatch_barrier_async
    6. 任务1和2异步执行,逆序输出(2的任务时间短)

    小结

    • 同步过程会阻塞当前的调用者线程,等待block中的任务执行完毕。除非必要,同步过程尽量不要用
    • 栅栏函数可以作为dispatch_group一种简化用法,推荐使用。用异步栅栏就可以了。同步栅栏没有必要,增加理解的难度。
    • 串行队列可以用在“线程安全的字典”等场景,比较方便
    • 单例和延迟执行很经典,推荐使用
    • 能用dispatch_group和栅栏函数解决问题的地方就不要用mutex,信号量等进程间同步的技术,一不小心就死锁了
    • 栅栏比NSOperationQueue的依赖还好用,循环依赖也会带来死锁问题的。不过还是建议使用NSOperationQueue,上层API在内存管理等方面更让人省心和放心

    相关文章

      网友评论

          本文标题:GCD学习

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