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

在上一篇文章中,我们对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);
});

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

相关链接

根据文中出现顺序

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容

  • 谈到iOS多线程,一般都会谈到四种方式:pthread、NSThread、GCD和NSOperation。其中,苹...
    攻城狮GG阅读 274评论 0 3
  • Managing Units of Work(管理工作单位) 调度块允许您直接配置队列中各个工作单元的属性。它们还...
    edison0428阅读 7,990评论 0 1
  • GCD笔记 总结一下多线程部分,最强大的无疑是GCD,那么先从这一块部分讲起. Dispatch Queue的种类...
    jins_1990阅读 761评论 0 1
  • Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的...
    韩七夏阅读 292评论 0 0
  • 这段日子,我比任何时候都渴望老去。我想看到自己垂垂老矣,白发苍苍的样子,想要一扭头就看到那个陪了我一辈子的人,想看...
    蠢然阅读 194评论 0 0