GCD的使用(1)使用GCD保护property

  作为一个iOS开发者,必须要熟练使用GCD,本文是站在实际应用的角度总结GCD的用法之一: 使用barrier保护property。在多线程环境下,如果有多个线程要执行同一份代码,那么有时会出现问题,为了保证某些资源操作的可控性,需要一些手段来对这些“公共资源”进行保护,无论是什么语言,只要支持多线程,都会面对这个问题,即所谓的“资源同步问题”,对于基于Objective-C的iOS开发而言,也许“同步问题”出现最多的资源是property。

atomic修饰词保护property

对property原子性最简单的保护措施是为其添加atomic修饰词,被atomic修饰的property,其setter方法默认为:

譬如:

@property (atomic, strong) NSString *name;

加了atomic,setter函数会变成下面这样:

- (void)setName:(NSString *)newName {
    {lock}
    if (_name != newName) {
        _name = newName;
    }
    {unlock}
}

P.S:这段代码是参考网友的说法,并不权威。
总之,借用网友的描述:

如果使用atomic,如其名,它会保证每次getter和setter的操作都会正确的执行完毕,而不用担心其它线程在你get的时候set,可以说保证了某种程度上的线程安全。但是,我上网查了资料,仅仅靠atomic来保证线程安全是很天真的。

此外,根据《Effective Objective-C 2.0》模糊的描述,似乎LLVM为atomic修饰的property生成的setter和getter似乎如下:

@property (atomic, strong) NSString *someString;
    
@synthesize someString = _someString;
    
// getter
- (NSString *)someString {
    @synchronized(self) {
        return _someString;
    }
}
    
// setter
- (void)setSomeString:(NSString *)someString {
    @synchronized(self) {
        _someString = someString;
    }
}

如果果真是这样,那么使用atomic维持property的getter和setter的线程安全是有问题的,有什么问题呢?如果self中有很多个使用atomic修饰的property,譬如self同时有两个被atomic修饰的property:property1和property2,那么意味着如果线程A正在访问property1的时候线程B想访问property2,则会被阻塞,直到线程A结束对property1的访问。简而言之,self的所有被atomic修饰的资源都共用一把“锁”,这显然比较呆笨。
可见,滥用@synchronized(self)会降低代码效率,因为共用同一把锁的那些同步块,都必须按顺序执行。所以网友说“仅仅靠atomic来保证线程安全是很天真的”。

NSLock保护property

使用NSLock也是可以保护property的,但似乎只能用户保护property的setter方法,并且,我总觉得@synchronized(self)的本质和使用NSLock是差不多的。

如下:

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSLock *lockForName;
    
- (void)setName:(NSString *)name {
    [self.lockForName lock];
    _name = name;
    [self.lockForName unlock];
}

总之一句话,使用NSLock实现property的线程安全,不靠谱!

serial dispatch queue保护property

将对property的读写方法都安排在同一个队列中,即可保证数据同步,如下:

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
    
@synthesize name = _name;
    
// create a serial dispatch queue
_serialQueue = dispatch_queue_create("com.zhangbuhuai.test", nil);
    
// getter
- (NSString *)name {
    __block NSString *localName;
    dispatch_sync(_serialQueue, ^{
        localName = _name;
    });
    return localName;
}
    
// setter
- (void)setName:(NSString *)name {
    dispatch_sync(_serialQueue, ^{
        _name = name;
    });
}

此模式的思路是:把setter和getter都安排在序列化的队列里执行,这样的话,所有针对属性的访问就都同步了。为了使代码块能够设置局部变量,getter中用到了__block语句,若是抛开这一点,这种写法比之前的那些更为整洁。全部加锁任务都在GCD中处理,而GCD是在相当深的底层来实现的,于是能够做许多优化。因此,开发者无需担心那些事,只要专心把访问方法写好就行了。

然而,还可以进一步优化,毕竟setter方法不一定非得是同步的。设置实例变量所用的block,并不需要向setter返回什么值。也就是说,setter代码可以改成下面这样:

// setter
- (void)setName:(NSString *)name {
    dispatch_async(_serialQueue, ^{
        _name = name;
    });
}

这次只是把同步派发改成了异步派发,从调用者的角度来看,这个小改动可以提升设置方法的执行速度(毕竟直接返回而不用等待block执行完成),而读取操作与写入操作依然会按顺序执行。但是这么改有一个坏处:如果测试一下程序的性能,那么可能发现这种写法比原来慢,因为执行异步派发时,需要拷贝block。若拷贝block所用的时间明显超过执行块所用时间,则这种做法将比原来更慢。
P.S:所以,setter的block设置为asynchronous或者synchronous全看setter的block的复杂度。

barrier保护property

其实在更多的时候,调用getter可以并发执行,而getter和setter之前不能并发执行。利用这个特点,还能写一些更快一些的代码。此时正可以体现出GCD写法的好处。用同步块或锁对象,是无法轻易实现出如下这种方案的,这次不用serial dispatch queue,而改用并发队列:

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
    
@synthesize name = _name;
    
// create a concurrent dispatch queue
_concurrentQueue = dispatch_queue_create("com.zhangbuhuai.test", 0);
    
// getter
- (NSString *)name {
    __block NSString *localName;
    dispatch_sync(_concurrentQueue, ^{
        localName = _name;
    });
    return localName;
}
    
// setter
- (void)setName:(NSString *)name {
    dispatch_async(_concurrentQueue, ^{
        _name = name;
    });
}

然而,如上这样的代码,还无法正确实现同步。所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所以读取与写入操作可能随时执行。而我们恰恰不想让这些操作随意执行。此问题用一个简单的GCD功能即可解决,它就是栅栏(barrier)。下列函数可以向队列中派发块,将其作为栅栏使用:

void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来的要处理的block是barrier block,那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。待栅栏块执行完成后,再按正常方式继续向下执行。

在本例中,可以用栅栏块来实现属性的setter方法。在设置方法中使用了栅栏块之后,对属性的读取操作依然可以并发执行,但是写入操作却必须单独执行了,如下图所示:

                 barrier block示意图

代码实现很简单:

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
    
@synthesize name = _name;
    
// create a concurrent dispatch queue
_concurrentQueue = dispatch_queue_create("com.zhangbuhuai.test", 0);
    
// getter
- (NSString *)name {
    __block NSString *localName;
    dispatch_sync(_concurrentQueue, ^{
        localName = _name;
    });
    return localName;
}
    
// setter
- (void)setName:(NSString *)name {
    dispatch_barrier_async(_concurrentQueue, ^{
        _name = name;
    });
}

测试一下性能,就会发现,这种做法肯定比使用串行队列要快。当然,将上述代码中的dispatch_barrier_async改为dispatch_barrier_sync也是没问题的,也可能会更高效,至于原因上文已经讲到了。在实际使用时,最好还是测一测每种做法的性能,然后从中选出最适合当前场景的方案。

参考:

  1. 《Effective Objective-C 2.0》
原文地址:https://www.cnblogs.com/FightingLuoYin/p/4522414.html