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学习(三)

    GCD学习一 GCD学习二 GCD学习三 常用函数: dispatch_set_target_queue disp...

  • 学习GCD看我就够了

    学习GCD看我就够了 学习GCD看我就够了

  • GCD学习(二)

    GCD学习一 GCD学习二 GCD学习三 一。队列有哪几种呢? 1.1、自定义的队列 :dispatch_queu...

  • 十一、ios线程调用学习

    常用线程方式:GCD 一、GCD学习 Grand Central Dispatch(GCD) 是 Apple 开发...

  • GCD学习(一)

    GCD学习一 GCD学习二 GCD学习三 我不会讲太多理论,这篇是我自己的理解。 举个例子: 汽车进入维修厂的关...

  • GCD 线程安全同步-信号量

    GCD 线程安全同步 学习、记录与分享 GCD 与 NSThread比较 GCD会自动利用更多的CPU内核、 会自...

  • iOS GCD的使用

    什么是GCD了解GCD前,需要了解的基础知识GCD的使用使用注意事项 -GCD学习前铺垫-什么是GCDGCD (G...

  • iOS GCD的使用

    本文的主要内容是: 什么是GCD 了解GCD前,需要了解的基础知识 GCD的使用 使用注意事项 -GCD学习前铺垫...

  • GCD的使用

    最近在学习GCD,所以在网上找了些资料,这里只是对自己学习的总结 1.概念 GCD的好处GCD可用于多核的并行运算...

  • 06进阶之路-多线程管理

    1. GCD相关 学习链接 GCD 简介 (多核编程管理线程) GCD 任务和队列(同步和异步任务 并发和串行队列...

网友评论

      本文标题:GCD学习

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