深入理解dispatch_sync
关于GCD的基础知识,之前写过一篇博客,详见GCD基础知识。虽然之前已经梳理过了,但对很多知识点的理解仍然不够透彻…写这篇博客的原因是在阅读AFNetworking代码时遇到一些奇怪的代码。
如下:
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLResponse *response,
id responseObject,
NSError *error))completionHandler
{
__block NSURLSessionDataTask *dataTask = nil;
dispatch_sync(url_session_manager_creation_queue(), ^{
dataTask = [self.session dataTaskWithRequest:request];
}); [self addDelegateForDataTask:dataTask completionHandler:completionHandler]; return dataTask;
}
这段代码是在AFURLSessionManager中定义的,用于创建Data Task(NSURLSessionDataTask实例),这段代码令我比较奇怪的地方在于:使用dispatch_sync同步派发的方式提交任务。这短短几行(6~13行)代码的逻辑是:
- 以同步派发的方式提交任务 — 创建一个NSURLSessionDataTask对象;
- 第11行代码会被阻塞,直到NSURLSessionDataTask对象创建成功;
- “创建一个NSURLSessionDataTask对象”这个block执行完成,继续执行11~13行代码;
为什么这么处理呢?为什么以dispatch_sync的方式处理“创建Data Task”呢?基于之前的知识,我的猜测如下:
- 为了效率,“创建Data Task”是一个相对比较耗时间的工作,dataTaskWithRequest:completionHandler:这个方法的上下文可能是main thread,所以不能让“创建Data Task”过长时间阻塞当前线程;
P.S:但根据我的理解,dispatch_sync总会阻塞当前线程,直到所提交的任务执行完成,难道是我哪里理解错了,不禁差生了疑惑“dispatch_sync(…, block)”中的block的执行线程是哪个?是dispatch_sync当前上下文的线程还是另外一个线程?好吧,得趁机把这个问题搞清楚。 - 为了其他目的,我尚且不知道的原因。
无论如何,我得对dispatch_sync有进一步的理解了。
dispatch_sync使用说明
关于dispatch_sync代码的执行逻辑,恐怕很多人都已经知道了。以如下代码为例:
NSLog(@"step1");
dispatch_sync(aDispatchQueue, ^{
NSLog(@"step2");
//block具体代码
});
NSLog(@"step3");
简而言之,dispatch_sync()中的block会被同步派发,其上下文会被阻塞,直到dispatch_block派发的block被执行完成,这段代码的执行结果一定是:
step1
step2
step3
但问题是,dispatch_sync()的block的执行线程和dispatch_sync上下文所对应的线程是一个线程吗?
关于这个问题曾在GCD基础知识中阐述过,也一直以为:
dispatch_sync所派发的block的执行线程和dispatch_sync上下文线程是同一个线程(无论上述代码中的aDispatchQueue是serial dispatch queue还是concurrent dispatch queue)。
但在阅读网络资源的时候发现有这么一种说法:
编译器会根据实际情况优化代码,所以有时候你会发现block其实还在当前线程上执行,并没用产生新线程。
P.S:这个说法来自于iOS多线程的初步研究(八)— dispatch队列。
显然,网友的这个说法的言外之意是“dispatch_sync中的block的执行线程可能就是dispatch_sync上下文所在的线程,也可能不是”。
不禁对自己之前的理解产生了怀疑。孰是孰非?查了很多的资料,但都没能得到一个确切的答案。后来询问了一些不是相对权威的前辈,他们都支持“dispatch_sync所派发的block的执行线程和dispatch_sync上下文线程是同一个线程”这种说法。
我也通过示例代码做了一些验证:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"current thread 1: %@", [NSThread currentThread]);
dispatch_sync(dispatch_queue_create("I.am.a.serial.dispatch.queue", DISPATCH_QUEUE_SERIAL), ^{
NSLog(@"current thread 2: %@", [NSThread currentThread]);
});
dispatch_sync(dispatch_queue_create("I.am.a.concurrent.dispatch.queue", DISPATCH_QUEUE_CONCURRENT), ^{
NSLog(@"current thread 3: %@", [NSThread currentThread]);
});
}
/* 执行结果
current thread 1: <NSThread: 0x7fd96b42ca50>{number = 1, name = main}
current thread 2: <NSThread: 0x7fd96b42ca50>{number = 1, name = main}
current thread 3: <NSThread: 0x7fd96b42ca50>{number = 1, name = main}
*/
示例代码的执行结果显然说明dispatch_sync中的block的执行线程总是和dispatch_sync所在的上下文是同一个线程。但我依然怀疑“是不是dispatch_sync中处理的事情太简单了?弄复杂一点看看”,于是我做了如下修改:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"current thread 1: %@", [NSThread currentThread]);
dispatch_sync(dispatch_queue_create("I.am.a.serial.dispatch.queue", DISPATCH_QUEUE_SERIAL), ^{
dispatch_queue_t aConcurrentDispatchQueue = dispatch_queue_create("he", DISPATCH_QUEUE_CONCURRENT);
__block long sum = ;
for (int i = ; i < ; ++i) {
dispatch_async(aConcurrentDispatchQueue, ^{
sum += i;
});
}
NSLog(@"current thread 2: %@", [NSThread currentThread]);
});
dispatch_sync(dispatch_queue_create("I.am.a.concurrent.dispatch.queue", DISPATCH_QUEUE_CONCURRENT), ^{
dispatch_queue_t aSerialDispatchQueue = dispatch_queue_create("ha", DISPATCH_QUEUE_SERIAL);
__block long sum = ;
for (int i = ; i < ; ++i) {
dispatch_sync(aSerialDispatchQueue, ^{
sum += i;
});
}
NSLog(@"current thread 3: %@", [NSThread currentThread]);
});
}
无论如何增加了dispatch_sync中的block的复杂程度。得到的结果并无区别。
如此这般,我想我可以认定之前的说法:
「dispatch_sync派发的block的执行线程总是和dispatch_sync所在的上下文是同一个线程」。
相信以后能发现更加权威的文档资料来佐证这个观点。
使用串行同步队列保护代码
回到本文开头部分的代码:
__block NSURLSessionDataTask *dataTask = nil;
dispatch_sync(url_session_manager_creation_queue(), ^{
dataTask = [self.session dataTaskWithRequest:request];
}); [self addDelegateForDataTask:dataTask completionHandler:completionHandler]; return dataTask;
上述url_session_manager_creation_queue()函数返回的其实是一个serial dispatch queue,这种组合即所谓的「串行同步队列」。
经过上文的分析,这段代码中使用「串行同步队列」创建NSURLSessionDataTask实例显然不是为了提高效率,那么它的意义是什么呢?
好吧,既然搞不懂,就查资料吧!
翻开《Effective Objective-C 2.0》,看到item41 — 《多用派发队列,少用同步锁》,我想,这段代码中使用「串行同步队列」的唯一解释是:为了保证[self.session dataTaskWithRequest:request]的线程安全。
至于为什么要保证[self.session dataTaskWithRequest:request]这个操作的线程安全,考虑到dataTaskWithRequest:这个方法是在iOS中定义的(只提供了接口,看不到源码),此处就不作详细分析了,我猜测这个方法不是「线程安全」方法吧。
dispatch_sync使用注意事项
学习GCD当然要读Apple的官方文档《Concurrency Programming Guide》,其中包括关于使用dispatch_sync的提示:
Important: You should never call the dispatch_sync or dispatch_sync_f function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.
简单来说,在dispatch_sync嵌套使用时要注意:不能在一个嵌套中使用同一个serial dispatch queue,因为会发生死锁;
假设有如下这么一段代码要执行:
- (void)test {
dispatch_queue_t aSerialDispatchQueue =
dispatch_queue_create("I.am.an.iOS.developer", DISPATCH_QUEUE_SERIAL);
dispatch_sync(aSerialDispatchQueue, ^{
// block 1
NSLog(@"Good Night, Benjamin.");
dispatch_sync(aSerialDispatchQueue, ^{
// block 2
NSLog(@"Good Night, Daisy.");
});
});
}
自己试着执行以下就会发现:Good Night, Daisy.这一句永远都无法被打印出来,原因很简单,程序产生了死锁。为什么会产生死锁呢?
可以想象aSerialDispatchQueue在底层实现中有一把“锁”,这把锁确保serial dispatch queue中只有一个block被执行,当执行到block 1代码时,这把锁为block 1所持有,当block 1执行完了,会释放之;然而block 1同步派发了一个任务block 2,同步派发意味着block 1会被阻塞,直到block 2被执行完成;但是这里产生了矛盾,block 2顺利执行的前提是aSerialDispatchQueue的这把“锁”被block 1释放,但是block 1释放这把“锁”的前提是block 1执行完成,而block 1执行完的前提是block 2执行完成;所以造成的局面是“block 2等待block 1执行完成置放‘锁’”,同时“block 1等待block 2执行完成”,这就是典型的deadlock。
这一段代码还好,比较容易避免,但是如果对GCD理解不深,更多的时候容会写出如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 巴拉巴拉,做了很多事情
NSLog(@"Good Night, Benjamin.");
dispatch_sync(dispatch_get_main_queue(), ^{
// refresh UI
NSLog(@"Good Night, Daisy.");
});
}
这段代码的问题其实和上一段代码类似,只不过这里的serial dispatch queue恰好是main queue。
上述的deadlock问题主要针对「同步串行队列」,对于「同步并行队列」,根据我的理解应该不存在这个deadlock问题,但是《Concurrency Programming Guide》明确说了:
…This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.
P.S:目前还不理解《Concurrency Programming Guide》中的这个说辞。
P.S:根据我的理解,「串行队列」在底层实现中应该有一把“锁”用来保证「串行队列」中的block有且仅有一个block被执行;但是「并行队列」的实现机制是啥呢?希望以后能回答这个问题。也许只有回答了这个问题,才能理解“… but should also be avoided for concurrent queues”。
参考资料:
- 《Effective Objective-C 2.0》;
- 博文《iOS多线程的初步研究(八)— dispatch队列》;
- 《Concurrency Programming Guide》;
深入理解dispatch_sync的更多相关文章
- GCD深入理解(1)
写在前面 本文原文为raywenderlich的<grand-central-dispatch-in-depth-part-1>:顺便提及一下,笔者认为,对于iOS初学者而言,raywen ...
- GCD的深入理解
GCD 深入理解(一) 本文由@nixzhu翻译至raywenderlich的<grand-central-dispatch-in-depth-part-1> 虽然 GCD 已经出现过一段 ...
- iOS开发:深入理解GCD 第二篇(dispatch_group、dispatch_barrier、基于线程安全的多读单写)
Dispatch Group在追加到Dispatch Queue中的多个任务处理完毕之后想执行结束处理,这种需求会经常出现.如果只是使用一个Serial Dispatch Queue(串行队列)时,只 ...
- coreData 深入理解4 --总结 (线程安全与同步--iOS5 前后对比)
Core Data是iOS中很重要的一个部分,可以理解为基于SQLite(当然也可以是其他的Storage,如In-memory,只是SQLite比较常见)的一个ORM实现,所以有关系数据库的特性,又 ...
- GCD 深入理解:第一部分
虽然 GCD 已经出现过一段时间了,但不是每个人都明了其主要内容.这是可以理解的:并发一直很棘手,而 GCD 是基于 C 的 API ,它们就像一组尖锐的棱角戳进 Objective-C 的平滑世界. ...
- 深入理解GCD(一)
虽然 GCD 已经出现过一段时间了,但不是每个人都明了其主要内容.这是可以理解的:并发一直很棘手,而 GCD 是基于 C 的 API ,它们就像一组尖锐的棱角戳进 Objective-C 的平滑世界. ...
- GCD 深入理解(一)
http://www.cocoachina.com/industry/20140428/8248.html 本文由@nixzhu翻译至raywenderlich的<grand-central-d ...
- iOS开发中GCD在多线程方面的理解
GCD为Grand Central Dispatch的缩写. Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法.在Mac OS X 10.6雪豹中 ...
- 深入理解 GCD
前言 首先提出一些问题: dispatch_async 函数如何实现,分发到主队列和全局队列有什么区别,一定会新建线程执行任务么? dispatch_sync 函数如何实现,为什么说 GCD 死锁是队 ...
随机推荐
- ZOJ - 4019 Schrödinger's Knapsack (背包,贪心,动态规划)
[传送门]http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5747 [题目大意]:薛定谔的背包.薛定谔的猫是只有观测了才知道猫的死 ...
- spring解决乱码
spring提供的工具类解决乱码问题 在web.xml配置中添加如下代码: <!--乱码处理--> <filter> <filter-name>encodingFi ...
- oracle学习 第二章 限制性查询和数据的排序 ——03
这里.我们接着上一小节2.6留下的问题:假设要查询的字符串中含有"_"或"%".又该如何处理呢? 開始今天的学习. 2.7 怎样使用转义(escape)操作符 ...
- 2017年记录CS+CV
2017年3月开学,始终感觉自己计算机基础薄弱,加上之前自己也开始对机器学习,深度学习有一些了解,始终感觉没有入门.自己开始规划系统学习计算机软件(CS)和计算机视觉(CV)的基础知识.@2017/9 ...
- java学习笔记(四)面向对象
一.形參长度可变的方法 当传入被调用的函数參数数量不确定时,在方法最后一个形參的类型后加上三个点号(...),表明该形參能够接受多个參数值.多个參数值被当做数组传入,这些參数必须为指定的类型. pac ...
- NGUI UIScrollView - 大量item子项的性能优化
一.当UIScrollView的以下的包括的子项太多(二三十个之上)时.它的滚动就会变的有些卡不流畅,尤其是在手机上. 对些网上也有非常多的优化它的相关,以下是我的一个优化: 1.将在超出裁剪框的一个 ...
- Android Camera 拍照 三星BUG总结
Android Camera 三星BUG : 近期在Android项目中使用拍照功能 , 其他型号的手机执行成功了 只有在三星的相机上遇到了bug . BUG详细体现为 : (1) 摄像头拍照后图 ...
- java:BufferedImage推断图像通道顺序并转RGB/BGR
一般来说java ImageIO处理读取图像时.通常是RGB或ARGB格式,可是有的时候.我们须要图像是BGR格式. 比方通过JNI将图像矩阵传递给动态库,动态库里用OpenCV来处理矩阵,而用Ope ...
- 常用SQL备忘录
联表删除: delete t1,t2 from table_name t1 left join t2 on t1.id=t2.id where t1.id=23 (ps:该语句在mysql 5.0之前 ...
- Apach Web Server区别于其他应用服务器的主要特点是什么?
Web服务器一般指的是处理静态请求或转发http请求的服务器,而应用服务器一般是用来处理动态请求的服务器.