iOS开发:深入理解GCD 第二篇(dispatch_group、dispatch_barrier、基于线程安全的多读单写)
- Dispatch Group
在追加到Dispatch Queue中的多个任务处理完毕之后想执行结束处理,这种需求会经常出现。如果只是使用一个Serial Dispatch Queue(串行队列)时,只要将想执行的处理全部追加到该串行队列中并在最后追加结束处理即可,但是在使用Concurrent Queue 时,可能会同时使用多个Dispatch Queue时,源代码就会变得很复杂。在这种情况下,就可以使用Dispatch Group。
1234567891011121314151617181920212223dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create(
"com.gcd-group.www"
, DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for
(
int
i = 0; i < 1000; i++) {
if
(i == 999) {
NSLog
(@
"11111111"
);
}
}
});
dispatch_group_async(group, queue, ^{
NSLog
(@
"22222222"
);
});
dispatch_group_async(group, queue, ^{
NSLog
(@
"33333333"
);
});
dispatch_group_notify(group, queue, ^{
NSLog
(@
"done"
);
});
控制台的输出:
因为向Concurrent Dispatch Queue 追加处理,多个线程并行执行,所以追加处理的执行顺序不定。执行顺序会发生变化,但是此执行结果的done一定是最后输出的。
无论向什么样的Dispatch Queue中追加处理,使用Dispatch Group都可以监视这些处理执行的结果。一旦检测到所有处理执行结束,就可以将结束的处理追加到Dispatch Queue中,这就是使用Dispatch Group的原因。
下面试一个使用Dispatch Group异步下载两张图片,然后合并成一张图片的medo(注意,我们总是应该在主线程中更新UI):
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667#import "ViewController.h"
@interface
ViewController ()
@property
(
nonatomic
, strong) UIImage *imageOne;
@property
(
nonatomic
, strong) UIImage *imageTwo;
@property
(
nonatomic
, weak) UILabel *textLabel;
@end
@implementation
ViewController
- (
void
)viewDidLoad {
[
super
viewDidLoad];
[
self
operation1];
}
- (
void
)operation1
{
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(200, 450, 0, 0)];
textLabel.text = @
"正在下载图片"
;
[textLabel sizeToFit];
[
self
.view addSubview:textLabel];
self
.textLabel = textLabel;
[
self
group];
NSLog
(@
"在下载图片的时候,主线程貌似还可以干点什么"
);
}
- (
void
)group
{
UIImageView *imageView = [[UIImageView alloc] init];
[
self
.view addSubview:imageView];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create(
"cn.gcd-group.www"
, DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog
(@
"正在下载第一张图片"
);
NSData
*data = [
NSData
dataWithContentsOfURL:[
NSURL
URLWithString:@
"http://images2015.cnblogs.com/blog/471463/201509/471463-20150912213125372-589808688.png"
]];
NSLog
(@
"第一张图片下载完毕"
);
self
.imageOne = [UIImage imageWithData:data];
});
dispatch_group_async(group, queue, ^{
NSLog
(@
"正在下载第二张图片"
);
NSData
*data = [
NSData
dataWithContentsOfURL:[
NSURL
URLWithString:@
"http://images2015.cnblogs.com/blog/471463/201509/471463-20150912212457684-585830854.png"
]];
NSLog
(@
"第二张图片下载完毕"
);
self
.imageTwo = [UIImage imageWithData:data];
});
dispatch_group_notify(group, queue, ^{
UIGraphicsBeginImageContext(CGSizeMake(300, 400));
[
self
.imageOne drawInRect:CGRectMake(0, 0, 150, 400)];
[
self
.imageTwo drawInRect:CGRectMake(150, 0, 150, 400)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
UIImageView *imageView = [[UIImageView alloc] initWithImage:newImage];
[
self
.view addSubview:imageView];
self
.textLabel.text = @
"图片合并完毕"
;
});
});
}
@end
- dispatch_barrier_async
在访问数据库或者文件的时候,我们可以使用Serial Dispatch Queue可避免数据竞争问题,代码如下所示:
先看看,如果我们在平常编码中,如果要保证某个属性可以线程安全的读写,如何写的:12345678910111213141516171819202122232425#import <Foundation/Foundation.h>
@interface
ZYPerson :
NSObject
@property
(
nonatomic
,
copy
)
NSString
*name;
@end
#import "ZYPerson.h"
static
NSString
*_name;
@implementation
ZYPerson
- (
void
)setName:(
NSString
*)name
{
@synchronized
(
self
) {
_name = [name
copy
];
}
}
- (
NSString
*)name
{
@synchronized
(
self
) {
return
_name;
}
}
@end
这是我在刚学iOS开发,刚涉及并发中的数据竞争时,书本上提到的一种解决方案。如果有多个线程要执行同一份代码,那么有时候可能会出现问题,这种情况下,通常要使用锁来实现某种同步机制。iOS提供了一种加锁的方式,就是采用内置的synchronization block,也就是上面代码所写的。
这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁也就释放了。在上面的例子中,同步行为所针对的对象是self。这么写通常没错,但是@synchronized(self)会大大降低代码效率,甚至很多时候,还可以被人感觉到效率明显下降了,因为共用同一个锁的那些同步块,都必须按顺序执行。若在self对象上频繁加锁,那么程序可能就要等另一段与此无关的代码执行完毕,才可以继续执行当前代码,这样做是很没必要的。
@synchronized(self)会大大降低代码效率,因为所有的同步块( @synchronized(self) )都会彼此抢夺同一个锁。要是有多个属性这么写,每个属性的同步块( @synchronized(self) )都要等其他所有的同步块执行完毕之后才能执行,这并不是我们想要的结果,我们只想要每个属性各自独立的同步。
还有,不得不说,按上面这么做,虽然可以在一定程度上提供“线程安全”,但却无法保证访问该对象时是绝对线程安全的。事实上,上面的写法,就是atomic,也就是原子性属性xcode自动生成的代码,这种方法,在访问属性时,必定可以从中得到有效值,然而如果在一个线程上多次调用getter方法,每次得到的结果却未必相同,在两次读操作之间,其他线程可能会写入新的属性值。
其实使用GCD可以简单高效的代替同步块或者锁对象,可以使用,串行同步队列,将读操作以及写操作都安排在同一个队列里,即可保证数据同步,代码如下:
123456789101112131415161718192021222324252627282930313233343536373839#import <Foundation/Foundation.h>
@interface
ZYPerson :
NSObject
@property
(
nonatomic
,
copy
)
NSString
*name;
@end
#import "ZYPerson.h"
@interface
ZYPerson ()
@end
static
NSString
*_name;
static
dispatch_queue_t _queue;
@implementation
ZYPerson
- (instancetype)init
{
if
(
self
= [
super
init]) {
_queue = dispatch_queue_create(
"com.person.syncQueue"
, DISPATCH_QUEUE_SERIAL);
}
return
self
;
}
- (
void
)setName:(
NSString
*)name
{
dispatch_sync(_queue, ^{
_name = [name
copy
];
});
}
- (
NSString
*)name
{
__block
NSString
*tempName;
dispatch_sync(_queue, ^{
tempName = _name;
});
return
tempName;
}
@end
这样写的思路是:把写操作与读操作都安排在同一个同步串行队列里面执行,这样的话,所有针对属性的访问操作就都同步了。
这种方法的确已经足够好了,但还不是最优的,它只可以实现单读、单写。整体来看,我们最终要解决的问题是,在写的过程中不能被读,以免数据不对,但是读与读之间并没有任何的冲突!多个getter方法(也就是读取)是可以并发执行的,而getter(读)与setter(写)方法是不能并发执行的,利用这个特点,还能写出更快的代码来,这次注意,不用串行队列,而改用并行队列:
12345678910111213141516171819202122232425262728293031323334353637#import <Foundation/Foundation.h>
@interface
ZYPerson :
NSObject
@property
(
nonatomic
,
copy
)
NSString
*name;
@end
#import "ZYPerson.h"
@interface
ZYPerson ()
@end
static
NSString
*_name;
static
dispatch_queue_t _concurrentQueue;
@implementation
ZYPerson
- (instancetype)init
{
if
(
self
= [
super
init]) {
_concurrentQueue = dispatch_queue_create(
"com.person.syncQueue"
, DISPATCH_QUEUE_CONCURRENT);
}
return
self
;
}
- (
void
)setName:(
NSString
*)name
{
dispatch_barrier_async(_concurrentQueue, ^{
_name = [name
copy
];
});
}
- (
NSString
*)name
{
__block
NSString
*tempName;
dispatch_sync(_concurrentQueue, ^{
tempName = _name;
});
return
tempName;
}
@end
这样优化,测试一下性能,可以发现这种做法肯定比使用串行队列要快。
在这个代码中,我用了点新的东西,dispatch_barrier_async,可以翻译成栅栏(barrier),它可以往队列里面发送任务(块,也就是block),这个任务有栅栏(barrier)的作用。
在队列中,barrier块必须单独执行,不能与其他block并行。这只对并发队列有意义,并发队列如果发现接下来要执行的block是个barrier block,那么就一直要等到当前所有并发的block都执行完毕,才会单独执行这个barrier block代码块,等到这个barrier block执行完毕,再继续正常处理其他并发block。在上面的代码中,setter方法中使用了barrier block以后,对象的读取操作依然是可以并发执行的,但是写入操作就必须单独执行了。
- 附录(也算是追加)
也许会对并行、串行、异步、同步有所理解不深。
并行:就是队列里面的任务(代码块,block)不是一个个执行,而是并发执行,也就是可以同时执行的意思
串行:队列里面的任务一个接着一个执行,要等前一个任务结束,下一个任务才可以执行
异步:具有新开线程的能力
同步:不具有新开线程的能力,只能在当前线程执行任务那么,如果他们相互串起来,会怎么样呢?
并行+异步:就是真正的并发,新开有有多个线程处理任务,任务并发执行(不按顺序执行)
串行+异步:新开一个线程,任务一个接一个执行,上一个任务处理完毕,下一个任务才可以被执行
并行+同步:不新开线程,任务一个接一个执行
串行+同步:不新开线程,任务一个接一个执行如此,并行+异步,串行+异步倒是很好辨别,他们有各自的特点,那么并行+同步,串行+同步,貌似效果是一样的,好像用哪种都一样??!!
不,这其中区别很大(不得不说一句,我所看多线程书籍,基本没讲到这点的,不知道是不是他们都认为这是基础,没必要说明)。这其中的区别,我觉得代码体现更直观,直接上代码吧:
这一份是并行+同步,我在外面并发添加代码块:1234567891011121314151617181920212223242526272829dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog
(@
"11 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog
(@
"22 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog
(@
"33 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog
(@
"44 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog
(@
"55 %@"
,[
NSThread
currentThread]);
});
});
看看打印:
在一个线程里面,任务一个接一个的执行。
再看看,并行+同步,并发调用:
1234567891011121314151617181920212223242526272829dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog
(@
"11 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog
(@
"22 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog
(@
"33 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog
(@
"44 %@"
,[
NSThread
currentThread]);
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog
(@
"55 %@"
,[
NSThread
currentThread]);
});
});
打印:
新开了多个线程,任务并发执行。
iOS开发:深入理解GCD 第二篇(dispatch_group、dispatch_barrier、基于线程安全的多读单写)的更多相关文章
- iOS开发:深入理解GCD 第一篇
最近把其他书籍都放下了,主要是在研究GCD.如果是为了工作,以我以前所学的GCD.NSOperation等知识已经足够用了,但学习并不仅仅知识满足于用它,要知其然.并且知其所以然,这样才可以不断的提高 ...
- iOS开发SDWebImageOptions理解
iOS开发SDWebImageOptions理解 原文 http://www.cnblogs.com/WJJ-Dream/p/5816750.html typedef NS_OPTIONS(NSUIn ...
- iOS GCD中级篇 - dispatch_group的理解及使用
上一篇GCD基础篇,以及同步.异步,并发.并行几个概率的理解 关于dispatch_group的概念以及几种场景下的使用 1.关于dispatch_group 把一组任务提交到队列中,这些队列可以不相 ...
- iOS GCD中级篇 - dispatch_group
1.关于dispatch_group 把一组任务提交到队列中,这些队列可以不相关,然后监听这组任务完成的事件. 最常见的几个方法: 1.dispatch_group_create创建一个调度任务组 2 ...
- iOS刨根问底-深入理解GCD
概述 做过iOS开发的同学相信对于GCD(Grand Central Dispatch)并不陌生,因为在平时多线程开发过程中GCD应该是使用最多的技术甚至它要比它的上层封装NSOperation还要常 ...
- iOS开发——网络使用技术OC篇&网络爬虫-使用正则表达式抓取网络数据
网络爬虫-使用正则表达式抓取网络数据 关于网络数据抓取不仅仅在iOS开发中有,其他开发中也有,也叫网络爬虫,大致分为两种方式实现 1:正则表达 2:利用其他语言的工具包:java/Python 先来看 ...
- iOS开发之多线程技术——NSOperation篇
本篇将从四个方面对iOS开发中使用到的NSOperation技术进行讲解: 一.什么是NSOperation 二.我们为什么使用NSOperation 三.在实际开发中如何使用NSOperation ...
- ios开发——常用经典算法OC篇&冒泡/快速
冒泡排序与快速排序 1.序言 ios开发中涉及到算法的地方还真不多,除非你的应用程序真的非常大,或者你想你的应用程序性能非常好才会去想到关于算法方面的性能优化,而在ios开发中真的能用得到的也就是关于 ...
- iOS开发 - Swift使用GCD实现计时器功能
前言 开发中,经常会用到定时执行网络请求.倒计时.计时器等功能,本篇文章介绍在iOS开发中,Swift怎样使用GCD实现这些功能. 执行一次 下面的代码将会在5秒后执行,且只执行一次. let tim ...
随机推荐
- JAVA虚拟机垃圾回收算法原理
除了释放不再被引用的对象外,垃圾收集器还要处理堆碎块.新的对象分配了空间,不再被引用的对象被释放,所以堆内存的空闲位置介于活动的对象之间.请求分配新对象时可能不得不增大堆空间的大小,虽然可以使用的总空 ...
- 两个二进制数多少个位(bit)不同
class Solution { public: /** * 获得两个整形二进制表达位数不同的数量 * * @param m 整数m * @param n 整数n * @return 整型 */ in ...
- springboot教程
http://www.cnblogs.com/java-zhao/tag/spring-boot/ http://blog.csdn.net/liaokailin/article/category/5 ...
- tar打包排除某个目录
tar zcvf fd.tar.gz * --exclude=file1 --exclude=dir1 注意: 1.--exclude=file1 而不是 --exclude file1 2.要排除一 ...
- NGUI之渲染DrawCall的合并
在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call.Draw Call值越低,会得到更好的渲染性能. (NGUI 查看DrawCall工具(NGUI-OPEN-Draw Ca ...
- SourceInsight 精确导入Linux kernel源码的方法
相信有很多人用 SourceInsight 查看 Linux Kernel 源码,但导入源码时会遇到一些问题.1.如果把整个源码树都导入进去,查看符号定义的时候,会发现有大量重复定义,很难找到正确的位 ...
- PHP使用feof()函数读文件的方法
这篇文章主要介绍了PHP使用feof()函数读文件的方法,以实例形式对比了正确与错误的用法,阐明了feof()函数的使用技巧,需要的朋友可以参考下 本文实例讲述了PHP使用feof()函数读文件的方法 ...
- Ubuntu中启用关闭Network-manager网络设置问题!
Ubuntu中启用关闭Network-manager网络设置问题! [Server版本] 在UbuntuServer版本中,因为只存有命令行模式,所以要想进行网络参数设置,只能通过修改/etc/net ...
- OpenWrt镜像编译和ipv6支持
离成功实现路由器刷OpenWrt.接入校园网差不多一年了.路由工作比较稳定,还是很满意的. 这次回来有个新发现:学校有原生ipv6支持,在win7和ubuntu下什么都不用设置,自动获取global ...
- hive和ORACLE语法对比