第46条:不要使用 dispatch_get_current_queue
本条要点:(作者总结)
- 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相关推荐
- MySQL8.0 小白入门 46条sql语句练习(学生表 教师表 课程表 分数表)
4 Tab for MySQL8.0 1.表说明 2.创建表 3.插入数据 4.查询语句(46条) 1.表说明 – 数据库版本:mysql8 – 数据表说明:student(学生表).teacher( ...
- 男人必看的46条忠告
1.大事坚持原则,小事学会变通. 2.告别网恋,相比之下家人介绍的对象还是可以看看,必竟知根知底比较把握,少走弯路. 3.曾经背叛过你的女人想回头,对其说不. 4.穿着假名牌,不如一身便装,但求干净整 ...
- vue中如何使用vi-for限制遍历的条数?只查询前三条、查询4-6条怎么实现?
" 大家好,我是雄雄,欢迎关注微信公众号:雄雄的小课堂. " 前言 今天整理个简单的功能,vue中的v-for如何限制遍历输出的数据,比如我想在一个存放10条数据的集合中只输出3条 ...
- 第46条:for-each循环优先于传统的for循环
for-each循环通过完全隐藏迭代器或者索引变量,避免混乱和出错的可能,适用于集合和数组和任何实现Iterable接口的对象. 使用传统for循环,容易出错: enum Face { ONE, TW ...
- 高效编写iOS方法-小结
本文是本人看完<高效编写iOS的52条方法>的一些小结及笔记. 第 1 条 1.使用消息结构的语言,其运行时所应执行的代码由运行环境来决定(--动态绑定):而使用函数调用的语言,则由编译器 ...
- java78条注意事项
这78条来源于<Effective Java>一书,因中文版翻译得实在难道,我就只留了个目录. 创建和销毁对象 第1条:考虑用静态工厂方法代替构造器 第2条:遇到多个构造器参数时要考虑用构 ...
- 她的癌细胞救了上亿条命,却很少有人知道她的名字
前言 NGS系列文章包括NGS基础.转录组分析 (Nature重磅综述|关于RNA-seq你想知道的全在这).ChIP-seq分析 (ChIP-seq基本分析流程).单细胞测序分析 (重磅综述:三万字 ...
- 宝山区助行业强主体稳增长若干政策措施的实施细则(20条)(征求意见稿)
今年以来,宝山区认真贯彻党中央.国务院.上海市重要决策部署,制定了一系列助企纾困政策,为巩固全区经济恢复发展基础发挥了积极作用.为持续放大政策组合迭加效应,结合全面恢复生产生活秩序后的新形势新情况,上 ...
- 有 50 家人家,每家一条狗。有一天警察通知, 50 条狗当中有病狗,行为和正常狗不一样。每人只能通过观察别 人家的狗来判断自己家的狗是否生病,而不能看自己家的狗,如果判断出自己家的狗病了,就必须当天
文章目录 题目 一.题目获得的条件 二.假设法解决题目 1.举例子 2.深入理解 总结 题目 有 50 家人家,每家一条狗.有一天警察通知, 50 条狗当中有病狗,行为和正常狗不一样.每人只能通过观察 ...
最新文章
- node python 速度_为什么python在递归上比node.js慢得多
- i.mx6ul 移植Openwrt
- matlab创建二叉树(二维数据)
- 多元回归求解 机器学习_金融领域里的机器学习算法介绍:人工神经网络
- RuiJi Scraper基础 – RuiJi表达式模型
- python输入年月日输出年月日_Python网站浪漫表白神器那些鲜为人知的技术
- java 27 - 7 反射之 通过反射越过泛型检查
- 服务器密闭通道天窗维修,机柜及密闭通道技术规范
- 16进制颜色码对照表
- 线性代数与空间解析几何重要知识点笔记
- 背单词App开发日记4
- OK3399设置GPIO默认低电平
- Python批量爬取华语天王巨星周杰伦的音乐
- “大数据杀熟”杀的是你吗?
- CUDA在VS中的环境搭建
- 学完java基础语法之后用来练习的不依赖框架的小项目
- 苹果鼠标怎么充电_“智能”还是“多功能”?米物智能鼠标垫测评
- 论文阅读:FlowNet 2.0: Evolution of Optical Flow Estimation with Deep Networks
- python创建网盘_超简单!基于Python搭建个人“云盘”
- 打开Jupyter报错:EnvironmentLocationNotFound: Not a conda environment
热门文章
- java ajax翻页_分页 工具类 前后台代码 Java JavaScript (ajax) 实现 讲解
- 最长公共前缀—leetcode14
- 全排列—leetcode46
- php tp 微信支付,PHP实现的微信APP支付功能示例【基于TP5框架】
- AndroidStudio 如何关闭 Install Run
- 中国软件开发工程师之痛
- php动态生成链接,PHP动态生成javascript文件的2个例子
- [数据结构] 二叉树基础
- 1976年,提出公钥密码体制概念的学者
- mysql在线开启并行复制_mysql 5.7开启并行复制