在访问数据库或者文件的时候,我们可以使用Serial Dispatch Queue可避免数据竞争问题,代码如下所示:
先看看,如果我们在平常编码中,如果要保证某个属性可以线程安全的读写,如何写的:
#import
@interfaceZYPerson :NSObject
@property(nonatomic,copy)NSString*name;
@end
#import "ZYPerson.h"
staticNSString*_name;
@implementationZYPerson
- (void)setName:(NSString*)name
{
@synchronized(self) {
_name = [namecopy];
}
}
- (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可以简单高效的代替同步块或者锁对象,可以使用,串行同步队列,将读操作以及写操作都安排在同一个队列里,即可保证数据同步,代码如下:
#import
@interfaceZYPerson :NSObject
@property(nonatomic,copy)NSString*name;
@end
#import "ZYPerson.h"
@interfaceZYPerson ()
@end
staticNSString*_name;
staticdispatch_queue_t _queue;
@implementationZYPerson
- (instancetype)init
{
if(self= [superinit]) {
_queue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_SERIAL);
}
returnself;
}
- (void)setName:(NSString*)name
{
dispatch_sync(_queue, ^{
_name = [namecopy];
});
}
- (NSString*)name
{
__blockNSString*tempName;
dispatch_sync(_queue, ^{
tempName = _name;
});
returntempName;
}
@end
这样写的思路是:把写操作与读操作都安排在同一个同步串行队列里面执行,这样的话,所有针对属性的访问操作就都同步了。
这种方法的确已经足够好了,但还不是最优的,它只可以实现单读、单写。整体来看,我们最终要解决的问题是,在写的过程中不能被读,以免数据不对,但是读与读之间并没有任何的冲突!
多个getter方法(也就是读取)是可以并发执行的,而getter(读)与setter(写)方法是不能并发执行的,利用这个特点,还能写出更快的代码来,这次注意,不用串行队列,而改用并行队列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37#import
@interfaceZYPerson :NSObject
@property(nonatomic,copy)NSString*name;
@end
#import "ZYPerson.h"
@interfaceZYPerson ()
@end
staticNSString*_name;
staticdispatch_queue_t _concurrentQueue;
@implementationZYPerson
- (instancetype)init
{
if(self= [superinit]) {
_concurrentQueue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
}
returnself;
}
- (void)setName:(NSString*)name
{
dispatch_barrier_async(_concurrentQueue, ^{
_name = [namecopy];
});
}
- (NSString*)name
{
__blockNSString*tempName;
dispatch_sync(_concurrentQueue, ^{
tempName = _name;
});
returntempName;
}
@end
这样优化,测试一下性能,可以发现这种做法肯定比使用串行队列要快。
在这个代码中,我用了点新的东西,dispatch_barrier_async,可以翻译成栅栏(barrier),它可以往队列里面发送任务(块,也就是block),这个任务有栅栏(barrier)的作用。
在队列中,barrier块必须单独执行,不能与其他block并行。这只对并发队列有意义,并发队列如果发现接下来要执行的block是个barrier block,那么就一直要等到当前所有并发的block都执行完毕,才会单独执行这个barrier block代码块,等到这个barrier block执行完毕,再继续正常处理其他并发block。在上面的代码中,setter方法中使用了barrier block以后,对象的读取操作依然是可以并发执行的,但是写入操作就必须单独执行了。