本条要点:(作者总结)

  • dispatch_get_current_queue 函数对行为常常与开发者所预期的不同。此函数已经废弃、止应做调试之用。
  • 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
  • dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用函数解决的问题,通常也能改用“队列特定数据”来解决。

  使用 GCD 时,经常需要判断当前代码正在哪个队列上执行,向多个队列派发任务时,更是如此。例如,Mac OS X 与 iOS 的 UI 事务都需要在主线程上执行,而这个线程就相当于 GCD 中的主队列。有时似乎需要判断出当前代码是不是在主队列上执行。阅读开发文档时,大家会发现下面这个函数:

1 dispatch_queue_t dispatch_get_current_queue()

  文档中说,此函数返回当前正在执行代码的队列。确实是这样,不过用的时候要小心。实际上,iOS 系统从 6.0 版本起,已经正式弃用此函数了。不过 Mac OS X 系统直到 10.8 版本也尚未将其废弃。虽说如此,但在 Mac OS X 系统里还是要避免使用它。

  该函数有种典型的错误用法(antipattern, “反模式”),就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇的死锁问题。考虑下面这两个存取方法,其代码用队列来证实对实例变量的访问操作是同步的:

 1   - (NSString *)someString {
 2
 3     __block NSString *localSomeString;
 4     dispatch_sync(_syncQueue, ^{
 5
 6       localSomeString = _someString;
 7     });
 8     return localSomeString;
 9   }
10
11
12
13   - (void)setSomeString:(NSString *)someString {
14
15     dispatch_async(_syncQueue, ^{
16
17       _someString = someString;
18     });
19   }

  这种写法的问题在于,获取方法(getter)可能会死锁,假如调用获取方法的队列恰好是同步操作所针对的队列(本例中是 _syncQueue),那么 dispatch_sync 就一直不会返回,直到块执行完毕为止。可是,应该执行块的那个目标队列却是当前队列,而当前队列的 dispatch_sync 又一直阻塞着,它在等待目标队列把这个块执行完,这样一来,块就永远没机会执行了。像 someString 这种方法,就是 “不可重入的”。

  看了 dispatch_get_current_queue 的文档后,你也许觉得可以用它改写这个方法,令其变得“可重入”,只需检测当前队列是否为同步操作所针对的队列,如果是,就不派发了,直接执行块即可:

 1   - (NSString *)someString {
 2
 3     __block NSString *localSomeString;
 4     dispatch_block_t accessorBlock = ^ {
 5       localSomeString = _someString;
 6     };
 7     if (dispatch_get_current_queue() == _syncQueue) {
 8       accessorBlock();
 9     } else {
10       dispatch_sync(_syncQueue, accessorBlock);
11     }
12
13     return localSomeString;
14   }

  这些做法可以处理一些简单的情况。不过仍然有死锁的危险。为说明原因,请读者考虑下面这段代码,其中有两个串行派发队列:

 1   dispatch_queue_t queueA = dispatch_queue_create("com.efftiveobjectivec.queueA", NULL);
 2
 3   dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
 4
 5   dispatch_sync(queueA, ^{
 6
 7     dispatch_sync(queueB, ^{
 8
 9       dispatch_sync(queueA, ^{
10
11         // Deadlock
12       });
13     });
14
15   });

  这段代码执行到最内层的派发操作时,总会死锁,因为此操作是针对  queueA 队列的,所以必须等最外层的 dispatch_sync 执行完毕才行(因为最外层的派发操作与最内层一样,也是针对 queueA 的),而最外层的那个 dispatch_sync 又不可能执行完毕,因为它要等最内层的 dispatch_sync 执行完,于是就死锁了。现在按照刚才的办法,使用 dispatch_get_current_queue 来检测:

 1   dispatch_sync(queueA, ^{
 2
 3     dispatch_sync(queueB, ^{
 4
 5       dispatch_block_t block = ^{/*...*/};
 6       if (dispatch_get_current_queue() == queueA) {
 7          block();
 8        } else {
 9         dispatch_sync(queueA, block);
10        } 
11
12     });
13
14   });

  然而这样做依然死锁,因为 dispatch_get_current_queue 返回的是当前队列,在本例中就是 queueB。这样的话,针对queueA 的同步派发操作依然会执行,于是和刚才一样,还是死锁了。

  在这种情况下,正确做法是:不要把存取方法做成可重入的,而是应该确保操作同步操作所用的队列绝不会访问属性,也就是绝对不会调用 someString 方法。这种队列只应该用来同步属性。由于派发队列是一种极为轻量的机制,所以,为了确保每项属性都有专用的同步队列,我们不妨创建多个队列。

  刚才那个例子似乎稍显做作,但是使用队列时还要注意另外一个问题,而那个问题会在你意想不到的地方导致死锁。队列之间会形成一套层级体系,这意味着排在某条队列中的块,会在其上级队列(parent queue,也叫“父队列”)里执行。层级里地位最高的那个队列总是 “全局并发队列”(global concurrentqueue)图描绘了一套简单的队列体系。

  排在队列B或队列C中的块,稍后会在队列A里依序执行。于是,排在队列A、B、C 中的块总是要彼此错开执行。然而,安排在队列D 中的块,则有可能与队列A 里的块(也包括队列B 与 队列C 里的块)并行,因为A 与 D 的目标队列是个并发队列。若有必要,并发队列可以用多个线程并行执行多个块,而是否会这样做,则需要根据 CPU 的核心数量等系统资源状况来定。

  由于队列间有层级关系,所以 “检查当前队列是否为执行同步派发所用的队列”这种办法,并不总是奏效。比方说,排在队列C里的块,会认为当前队列就是队列C,而开发者可能据此认定:在队列A上能够安全的执行同步派发操作。但实际上,这么做依然会像前面那样导致死锁。

  有的 API 可令开发者指定运行回调块时所用的队列,但实际上却会把回调块安排在内部的串行队列上,而内部队列的目标队列又是开发者所提供的那个队列,在此情况下,也许就要出现刚才说的那种问题了。使用这种 API 的开发者可能误以为:在回调块里调用 dispatch_get_current_queue 所返回的 “当前队列”,总是其调用API时指定的那个。但实际上返回的却是API内部的那个同步队列。

  要解决这个问题,最好的办法就是通过 GCD 所提供的功能来设定“队列特有数据”(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达根队列为止。笔者这么说,大家也许还不太明白其用法,所以看下面这个例子:

 1   dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
 2
 3   dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
 4
 5   dispatch_set_target_queue(queueB, queueA);
 6
 7   
 8
 9   static int kQueueSpecific;
10
11   CFStringRef queueSpecificValue = CFSTR("queueA");
12
13   dispatch_queue_set_specific(queueA, &kQueueSpecific, (void*)queueSpecificValue,(dispatch_function_t)CFRelease);
14
15   dispatch_sync(queueB, ^{
16
17     dispatch_block_t block = ^{ NSLog(@"No deadlock!");};
18     CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
19     if (retrievedValue) {
20       block();
21     } else {
22
23       dispatch_sync(queueA, block);
24     }
25
26 });    

  本例创建了两个队列。代码中将队列B的目标队列设为队列A,而队列A的目标队列仍然是默认优先级的全局并发队列。热后使用下列函数,在队列A上设置“队列特定值”:

1   void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);

  此函数的首个参数表示待设置数据队列,其后面两个参数是键与值。键与值都是不透明的void 指针。对于键来说,有个问题一定要注意:函数是按指针值来比较键的,而不是按照其内容。所以,“队列特定数据”的行为与 NSDictionary 对象不同,后者是比较键的 “对象等同性”。“队列特定数据”更像是关联引用。值(在函数原型里叫做 “context”(中文称为“上下文”、“语境”、“环境参数”等))也是不透明的void 指针,于是可以在其中存放任意数据。然而,必须管理该对象的内存。这使得在ARC 环境下很难使用Objective-C 对象作为值。范例代码使用 coreFoundation 字符串作为值,因为ARC 并不会自动管理CoreFoundation 对象的内存。所以说,这种对象非常适合充当“队列特定数据”,它们可以根据需要与相关的Objective-C Foundation 类无缝衔接。

  函数的最后一个参数是“析构函数”(destructor function),对于给定的键来说,当队列所占内存为系统所回收,或者有新的值与键相关联时,原有的值对象就会移除,而析构函数也会于此时运行。dispatch_function_t 类型的定义如下:

1 typedef void (*dispatch_function_t) (void *)

  由此可知,析构函数只能带有一个指针参数且返回值必须为 void。范例代码采用 CFRelease 做析构函数,此函数符合要求,不过也可以采用开发者自定义的函数,在其中调用 CFRelease 以清理旧值,并完成其他必要的清理工作。

  于是,“队列特定数据”所提供的这套简单易用的机制,就避免了使用 dispatch_get_current_queue 时经常遭遇的一个陷阱。此外,调试程序时也许会经常用到 dispatch_get_current_queue。在此情况下,可以放心的使用这个已经废弃的方法,只是别把它编译到发行版本的程序里就行。如果对“访问当前队列” 这项操作有特殊需求,而现有函数又无法满足,那么最好还是联系苹果公司,请求其加入此功能。

  END

转载于:https://www.cnblogs.com/chmhml/p/7421050.html

第46条:不要使用 dispatch_get_current_queue相关推荐

  1. MySQL8.0 小白入门 46条sql语句练习(学生表 教师表 课程表 分数表)

    4 Tab for MySQL8.0 1.表说明 2.创建表 3.插入数据 4.查询语句(46条) 1.表说明 – 数据库版本:mysql8 – 数据表说明:student(学生表).teacher( ...

  2. 男人必看的46条忠告

    1.大事坚持原则,小事学会变通. 2.告别网恋,相比之下家人介绍的对象还是可以看看,必竟知根知底比较把握,少走弯路. 3.曾经背叛过你的女人想回头,对其说不. 4.穿着假名牌,不如一身便装,但求干净整 ...

  3. vue中如何使用vi-for限制遍历的条数?只查询前三条、查询4-6条怎么实现?

    " 大家好,我是雄雄,欢迎关注微信公众号:雄雄的小课堂. " 前言 今天整理个简单的功能,vue中的v-for如何限制遍历输出的数据,比如我想在一个存放10条数据的集合中只输出3条 ...

  4. 第46条:for-each循环优先于传统的for循环

    for-each循环通过完全隐藏迭代器或者索引变量,避免混乱和出错的可能,适用于集合和数组和任何实现Iterable接口的对象. 使用传统for循环,容易出错: enum Face { ONE, TW ...

  5. 高效编写iOS方法-小结

    本文是本人看完<高效编写iOS的52条方法>的一些小结及笔记. 第 1 条 1.使用消息结构的语言,其运行时所应执行的代码由运行环境来决定(--动态绑定):而使用函数调用的语言,则由编译器 ...

  6. java78条注意事项

    这78条来源于<Effective Java>一书,因中文版翻译得实在难道,我就只留了个目录. 创建和销毁对象 第1条:考虑用静态工厂方法代替构造器 第2条:遇到多个构造器参数时要考虑用构 ...

  7. 她的癌细胞救了上亿条命,却很少有人知道她的名字

    前言 NGS系列文章包括NGS基础.转录组分析 (Nature重磅综述|关于RNA-seq你想知道的全在这).ChIP-seq分析 (ChIP-seq基本分析流程).单细胞测序分析 (重磅综述:三万字 ...

  8. 宝山区助行业强主体稳增长若干政策措施的实施细则(20条)(征求意见稿)

    今年以来,宝山区认真贯彻党中央.国务院.上海市重要决策部署,制定了一系列助企纾困政策,为巩固全区经济恢复发展基础发挥了积极作用.为持续放大政策组合迭加效应,结合全面恢复生产生活秩序后的新形势新情况,上 ...

  9. 有 50 家人家,每家一条狗。有一天警察通知, 50 条狗当中有病狗,行为和正常狗不一样。每人只能通过观察别 人家的狗来判断自己家的狗是否生病,而不能看自己家的狗,如果判断出自己家的狗病了,就必须当天

    文章目录 题目 一.题目获得的条件 二.假设法解决题目 1.举例子 2.深入理解 总结 题目 有 50 家人家,每家一条狗.有一天警察通知, 50 条狗当中有病狗,行为和正常狗不一样.每人只能通过观察 ...

最新文章

  1. node python 速度_为什么python在递归上比node.js慢得多
  2. i.mx6ul 移植Openwrt
  3. matlab创建二叉树(二维数据)
  4. 多元回归求解 机器学习_金融领域里的机器学习算法介绍:人工神经网络
  5. RuiJi Scraper基础 – RuiJi表达式模型
  6. python输入年月日输出年月日_Python网站浪漫表白神器那些鲜为人知的技术
  7. java 27 - 7 反射之 通过反射越过泛型检查
  8. 服务器密闭通道天窗维修,机柜及密闭通道技术规范
  9. 16进制颜色码对照表
  10. 线性代数与空间解析几何重要知识点笔记
  11. 背单词App开发日记4
  12. OK3399设置GPIO默认低电平
  13. Python批量爬取华语天王巨星周杰伦的音乐
  14. “大数据杀熟”杀的是你吗?
  15. CUDA在VS中的环境搭建
  16. 学完java基础语法之后用来练习的不依赖框架的小项目
  17. 苹果鼠标怎么充电_“智能”还是“多功能”?米物智能鼠标垫测评
  18. 论文阅读:FlowNet 2.0: Evolution of Optical Flow Estimation with Deep Networks
  19. python创建网盘_超简单!基于Python搭建个人“云盘”
  20. 打开Jupyter报错:EnvironmentLocationNotFound: Not a conda environment

热门文章

  1. java ajax翻页_分页 工具类 前后台代码 Java JavaScript (ajax) 实现 讲解
  2. 最长公共前缀—leetcode14
  3. 全排列—leetcode46
  4. php tp 微信支付,PHP实现的微信APP支付功能示例【基于TP5框架】
  5. AndroidStudio 如何关闭 Install Run
  6. 中国软件开发工程师之痛
  7. php动态生成链接,PHP动态生成javascript文件的2个例子
  8. [数据结构] 二叉树基础
  9. 1976年,提出公钥密码体制概念的学者
  10. mysql在线开启并行复制_mysql 5.7开启并行复制