从头认识GCD——相关函数的使用

字数 2045阅读 219

在上一篇文章中,我们对GCD有了基本的认知,知道其中一些简单的类型,和一些简单函数。这本篇文章中,我们将继续学习GCD中我们在日常开发中使用较多的函数,及其使用方法。在本篇会介绍___ dispatch_after、dispatch_apply、dispatch_group_t、dispatch_semaphore_t和dispatch_barrier ___等相关函数。

dispatch_after/dispatch_time_t

我先来说说___ dispatch_after ,从某种意义上来说,它属于任务提交的一种方式。在刚刚接触iOS开发的时候,我一直在想“ 对于dispatch_after它是同步提交代码块还是异步提交的代码块的呢? ”。后来看到Apple的文档中说到"This function waits until the specified time and then asynchronously adds block to the specified queue",也就是说它的延迟执行,并不是马上就将代码块就提交到指定的队列中,而是等到指定的时间通过异步的方式将提其提交到指定的队列中去_。因此从这段话中也可以看出它仅仅是dispatch_async的一种。该函数的声明如下:

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

到这里就需要来系统地说一说dispatch_after函数的第一个参数,一个dispatch_time_t类型的变量。dispatch_time_t实际是uint64_t类型。系统为该类型定义了两个特殊值,分别是__ DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER __,其中DISPATCH_TIME_NOW表示值为0,而DISPATCH_TIME_FOREVER表示为无穷大(infinity)。除了这两个特殊值之外,我们可以使用函数dispatch_time()来创建相对于默认时钟的时间;或者使用dispatch_walltime()函数获取绝对时间。
对于dispatch_time()函数,第一个参数我们传入DISPATCH_TIME_NOW或者DISPATCH_TIME_FOREVER值。

dispatch_time()函数第二个参数接受的是__ 基于纳秒级别的数值 __。

这时候就需要将具体的数字乘以一个常数,在官方文档中列出了相关的常数。

常数 意义 具体数值
NSEC_PER_SEC 表示一秒能转换成多少纳秒 1000000000ull
USEC_PER_SEC 表示一秒能转换成多少微秒 1000000ull
NSEC_PER_USEC 表示一微秒转换成多少纳秒 1000ull
/// 使用相对时间,相对于现在延迟五秒
dispatch_time_t time_t = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
dispatch_after(time_t, dispatch_get_main_queue(), ^{
        NSLog(@"Run");
});

如果我们想要该代码块延迟到某一指定时刻去执行,我们只需要去修改dispatch_after中的dispatch_time_t类型中值,在这里我们使用函数dispatch_walltime来获取绝对的时间戳值。dispatch_walltime()函数的一个参数是struct timespec类型的一个变量,它是一个结构:

_STRUCT_TIMESPEC
{
    __darwin_time_t  tv_sec;
    long  tv_nsec;
};

分别为秒和纳秒。timespec是基于纳秒级别的数值,关于dispatch_walltime具体是方式之一如下:

/// 延迟到某一绝对时刻执行
struct timespec __tp;
double sec, n_sec;
n_sec = modf(1500794750.797543543, &sec);
__tp.tv_sec = sec;
__tp.tv_nsec = n_sec;
dispatch_after(dispatch_walltime(&__tp, 0), dispatch_get_main_queue(), ^{
        ...
});

上诉代码要等到时间戳为1500794750时才会将代码块提交到指定的事件队列中。

dispatch_apply

___ dispatch_apply ___是dispatch_sync函数配合不同的的dispatch_queue_t队列,来循环执行任务。

如果在dispatch_apply函数中传入的是一个并发队列,那么block中的任务就可以被并发的调用!相对于一般的for循环来说要高效许多。

dispatch_queue_t apply_queue = dispatch_queue_create("com.example.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0));
dispatch_apply(5, apply_queue, ^(size_t index) {
        NSLog(@"%zd",index);
});
NSLog(@"End");

结果如下0, 2, 3, 1, 4, End。但是我们将上面的并发队列改成串行队列之后:

dispatch_queue_t apply_queue = dispatch_queue_create("com.example.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0));
dispatch_apply(5, apply_queue, ^(size_t index) {
        NSLog(@"%zd",index);
});
NSLog(@"End");

返回的结果0, 1, 2, 3, 4, End和正常的for循环没有什么差距。但是不管是在并发的队列还是在串行的队列中,End总是最后才打印的。

dispatch_group_t相关函数

使用dispatch_group可以把许多操作进行合并。在将多个任务block提交之后,我们可以在dispatch_group中获取到这些操作全部完成的时间(不管是串行执行还是并行执行)。
现在我们有一个场景:第一步,我们需要将多个本地资源传递给服务器。我们用dispatch_group相关的技术来实现这个需求。创建一个dispatch_group_t类型的变量实现非常简单,不像其他GCD函数需要一些其他的参数:

dispatch_group_t upload_group = dispatch_group_create();

当创建好了dispatch_group之后,我们需要将这些任务进行提交,这里我使用上一节的dispatch_apply来将多个任务放在并发的队列中:

dispatch_queue_t upload_queue = dispatch_queue_create("com.example.upload.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0));
dispatch_apply(5, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(size_t index) {
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), upload_queue, ^{
  /// 模拟网络请求
    NSLog(@"Upload %zd",index);
  });
});

在大部分的应用中的上传请求,都有一个上传完成的标志。第二步,那么在这个场景中我们如何知道所有图片已经上传成功呢?我们使用同步的方式,用户的交互不起作用,静静地等待上传完成:

dispatch_group_t upload_group = dispatch_group_create();
dispatch_queue_t upload_queue = dispatch_queue_create("com.example.upload.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0));
dispatch_apply(5, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(size_t index) {
        dispatch_group_enter(upload_group);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), upload_queue, ^{/// 模拟网络请求
            NSLog(@"Upload %zd",index);
            dispatch_group_leave(upload_group);
        });
});
dispatch_group_wait(upload_group, DISPATCH_TIME_FOREVER);
NSLog(@"Upload Complete");

dispatch_group的管理是基于计数来做的。dispatch_group_enter会增加该Group内部的任务计数,dispatch_group_leave会减少该Group中未完成的计数,它们两个函数必须配对使用。
dispatch_group_wait函数和我们在上一篇文中讲到的dispatch_block_wait函数功能类似,只不过dispatch_group_wait是针对多个block的同步方法,它会等到Group中所有的任务执行完毕之后才会去继续执行后面的内容。
  既然上面提到了dispatch_group_wait函数对应dispatch_block_wait函数,那么很明显应该存在dispatch_block_notify函数对应的Group函数。我们将上面的函数进行稍加改动,将同步的方式改为异步的方式,让用户能够做其他的操作:

dispatch_group_t upload_group = dispatch_group_create();
dispatch_queue_t upload_queue = dispatch_queue_create("com.example.upload.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0));
dispatch_apply(5, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(size_t index) {
        dispatch_group_enter(upload_group);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), upload_queue, ^{/// 模拟网络请求
            NSLog(@"Upload %zd",index);
            dispatch_group_leave(upload_group);
        });
});
dispatch_group_notify(upload_group, dispatch_get_main_queue(), ^{
        NSLog(@"Upload Complete");
});

其实相对于使用繁琐的dispatch_group_enter、dispatch_group_leave,Apple给我们提供了更为简单的函数dispatch_group_async。我这样做的目的是为了在一开始就能让我们清楚,在Group内部是什么在决定着dispatch_group_wait 、dispatch_group_notify的触发时机,我们还是对上面的例子进行稍加修改:

dispatch_group_t upload_group = dispatch_group_create();
dispatch_queue_t upload_queue = dispatch_queue_create("com.example.upload.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0));
dispatch_apply(5, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(size_t index) {
        dispatch_group_async(upload_group, upload_queue, ^{
            NSLog(@"Upload %zd",index);
        });
});
dispatch_group_notify(upload_group, dispatch_get_main_queue(), ^{
        NSLog(@"Upload Complete");
});

很明显对于使用dispatch_group_async给我们带来便利的同时,在灵活性上也就出现缺失,再者就是在用Group做同步的时候使用dispatch_group_enter、dispatch_group_leave是更好的选择!

dispatch_semaphore_t相关函数

在系统中,给予每一个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来(来自维基百科)。通俗一点儿讲就是说在进程内部有一原子递增和递减的计数器(也就是该数据变量具有原子性)。
如果触发了某个操作使得信号量小于等于0,那么该操作将会被阻塞,直到其信号量大于0。上面提到过,信号量是基于进程的。所以:

信号量不依赖于任何队列,它可以在任何线程中使用。

在GCD中,函数dispatch_semaphore_signal增加信号量计数,如果之前信号量计数小于等于0,该函数会唤醒当前正在等待的线程。相反,函数dispatch_semaphore_wait会减少信号量计数,如果当该信号量计数小于或者等于0之后,会阻塞当前线程,等待其他操作来增加信号量计数。

- (NSArray *)downloadSync{
    NSMutableArray *contents = [NSMutableArray array];
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    dispatch_group_t upload_group = dispatch_group_create();
    dispatch_queue_t upload_queue = dispatch_queue_create("com.example.download.gcd", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0));
    dispatch_apply(5, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(size_t index) {
        dispatch_group_enter(upload_group);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), upload_queue, ^{
            NSString *cts = [NSString stringWithFormat:@"%zd",index];
            NSLog(@"~ %@ ~",cts);
            [contents addObject:cts];
            dispatch_group_leave(upload_group);
        });
    });
    dispatch_group_notify(upload_group, dispatch_get_main_queue(), ^{
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return contents;
}

我们现在来看看上面这个方法可以正常的返回吗?除了dispatch_semaphore_t相关的代码,我都是直接从上面拷贝下来,没有做任何修改。当我跑起来之后,始终方法downloadSync不会返回,这里很明显的是造成了死锁的问题!由于dispatch_semaphore_wait函数会阻塞当前线程(它此时是处于主线程中),dispatch_group_notify函数的任务线程即为主线程对应的主任务队列。dispatch_semaphore_wait需要等到函数dispatch_semaphore_signal来增加信号量计数之后才会继续执行主线程,而dispatch_group_notify又要在主线程中执行(由于主线程被阻塞)之后才能去调用dispatch_semaphore_signal函数,因此就造成了死锁,程序永远不会继续执行!。
解决办法也很简单,将dispatch_semaphore_signal放在一个并行的任务队列中进行:

dispatch_group_notify(upload_group, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        dispatch_semaphore_signal(semaphore);
});

上面使用信号量的相关函数,实现了异步转同步的需求。

相关链接

根据文中出现顺序

推荐阅读更多精彩内容

  • 巧谈GCD字数4076 阅读1990 评论32 喜欢70谈到iOS多线程,一般都会谈到四种方式:pthread、N...
  • 谈到iOS多线程,一般都会谈到四种方式:pthread、NSThread、GCD和NSOperation。其中,苹...
  • Managing Units of Work(管理工作单位) 调度块允许您直接配置队列中各个工作单元的属性。它们还...
  • GCD笔记 总结一下多线程部分,最强大的无疑是GCD,那么先从这一块部分讲起. Dispatch Queue的种类...
  • Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的...