本文从《Effective Objective-C 2.0》开始整理,通过分析比较不同同步锁的优缺点,用GCD方法一步步找到了更高效的同步锁。
在Objective-C中,如果多个线程执行相同的代码,则会出现线程安全问题。 首先,我们来看看什么时候会发生线程安全问题。
线程安全
如果代码所在的进程同时运行多个线程,则这些线程可能会同时运行。 如果多个线程每次运行的结果与单线程运行的结果相同,并且其他变量的值也与预期的相同,则是线程安全的。
可读写的全局变量和静态变量以及Objective-C还包含属性和实例变量。 因为这些变量可以在不同的线程中修改,所以两者通常会引起线程安全问题。
对象- c同步锁定
在Objective-C中,如果多个线程执行相同的代码,则可能会出现线程安全问题。 在这种情况下,需要使用所实现某种同步机构。
在GCD出现之前,有两种方法。 一种方法是采用内置“同步块”(synchronization block ),另一种方法是使用锁定对象。
同步块(synchronization block ) )。
-(void )同步方法{
@synchronized(self ) {
//Safe
}
}
这种编写方法会根据给定的对象自动创建锁,并等待块中的代码执行。 运行到此代码的末尾将解除锁定。
这种同步方法的优点是无需在代码中显式创建锁定对象就可以实现锁定机制。 但是,滥用@synchronized(self )会降低代码效率,因为共享同一锁的同步块必须按顺序执行。 如果频繁地在self对象中锁定,则程序将在继续执行当前代码之前等待其他与之无关的代码的执行,从而降低效率。
注意: @synchronized(self )方法对self只有一个锁,因此使用self同步块的所有地方都使用相同的公共锁。 因此,如果存在多个同步块,其他同步块将等待当前同步块的执行完成。
-(void ) synchronizedAMethod { (
@synchronized(self ) {
//Safe
}
}
-(void ) synchronizedBMethod {
@synchronized(self ) {
//Safe
}
}
-(void )同步cmethod {
@synchronized(self ) {
//Safe
}
}
如果当前正在执行synchronizedAMethod方法,则在synchronizedBMethod完成之前不会执行上述代码,而是同时执行
锁定对象
@property(nonatomic,strong ) NSLock *lock;
_lock=[[NSLock alloc] init];
-(void )同步方法{
[_lock lock];
//Safe
[_lock unlock];
}
以上是单纯锁定对象的实现方式,但是如果弄错锁定的使用方法就会发生死锁现象,可以使用名为NSRecursiveLock的“递归锁定”(recursive lock )。
除以上锁定对象外,还有NSConditionLock条件锁定和NSDistributedLock分布式锁定。 它们适用于不同的场景,但不在此展开。
这些锁在使用时还存在缺陷。 在极端情况下,同步块会导致死锁,而且效率不一定很高,如果直接使用锁定对象的话,遇到死锁的话会非常麻烦。
GCD锁
在开始谈论GCD锁之前,让我们先了解一下GCD中的任务发送和队列。
交给任务
任务分发方法
说明
dispatch_sync ()
执行同步,等待计划任务完成,然后返回以阻止当前线程
dispatch_async ()
异步运行会立即返回,计划任务会完成,但不会等待完成,也不会阻塞当前线程
队列类型
队列类型
说明
串行队列
一次只能执行一个任务,必须等待上一个执行任务完成
并发队列
您可以一次同时执行多个任务,而无需等待正在运行的任务完成
GCD队列类型
GCD队列类型
获取方法
队列类型
说明
主队列
dispatch_get_main_queue
串行队列主线中执行
全局队列
dispatch_get_global_queue
并发队列
子线程中执行
用户队列
dispatch_queue_create
串并都可以
子线程中执行
以前同步锁的实现方式
在Objective-C中,属性就是开发者经常需要同步的地方。通常开发者想省事的话(以前我也是这样觉得),会这样写:
- (NSString *)someString {
@synchronized (self) {
return _someString;
}
}
- (void)setSomeString:(NSString *)someString {
@synchronized (self) {
_someString = someString;
}
}
以上代码除了上文提到的效率低以外,还有一个问题,就是该方法并不能保证访问该对象时绝对是线程安全的。虽然,这种方法在访问属性时,确实是“原子”的,也必定能从中获取到有效值,然而在同一线程上多次调用getter方法,每次获取到的结果未必相同。在两次访问操作之间,其他线程可能会写入新的属性值。此时,只能保证读写操作是“原子”的,而多个线程的执行顺序,我们没有办法控制。
使用GCD串行队列来实现同步锁
有种简单而高效的方法可以替代同步块或锁对象,那就是使用“串行同步队列”。将读取操作以及写入操作都安排在同一个队列里,即可保证数据同步。
用法如下:
@property (nonatomic,strong) dispatch_queue_t syncQueue;
_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue", NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return _someString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
此模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。
注:getter方法中,用一个临时变量来保存值,是因为在block中return的话,只是return到block中了,没有真正返回到对应的getter方法中,而__block是为了可以在block中改变改临时变量而用。
虽然问题解决了,但是我们还可以进一步优化。设置方法不一定非得是同步的。设置实例变量所用的块,并不需要向设置方法返回什么值。那代码可以改成:
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
这次把同步改成了异步,也许看来,这样改动,性能是会有提升的,但是你测一下程序的性能,可能会发现这种写法比原来慢。因为执行异步派发时,是需要拷贝块。若拷贝块所用的时间明显超过执行块所需的时间,则这种做法将比原来的更慢。
注:本例子代码比较简单,若是要执行的块代码逻辑比较复杂的话,那么该写法可能还是比原来的块些
使用GCD并发队列来实现同步锁
对于属性的读写,我们希望多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,利用这个特点,还能写出更快一些的代码来。此时正可体现出GCD的好处。而用同步锁或锁对象,是无法轻易实现下面这种方案的。这次我们使用并发队列:
@property (nonatomic,strong) dispatch_queue_t syncQueue;
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
以上代码,还无法正确实现同步。因为所有读写操作都会在同一个队列上执行,而该队列是并发队列,所有读取和写入操作都可以随时执行,没有达到同步效果。此问题我们可以通过一个简单的GCD功能解决--栅栏(barrier)。下列函数可以向队列中派发块,将其作为栅栏使用:
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
注:dispatch_barrier_async如果传入自己创建的并行队列时,会阻塞当前队列执行,而不阻塞当前线程。
dispatch_barrier_sync如果传入自己创建的并行队列时,阻塞当前队列的同时也会阻塞当前线程,请注意
并发队列如果发现接下来要处理的块是个栅栏块,那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。这待栅栏块执行完毕,在按正常方式继续向下处理。这样就解决了并发队列的同步问题。
GCD并发队列中加入栅栏
本例中,可以用栅栏块来实现属性的设置方法。在设置方法中使用了栅栏块之后,对属性的读取操作依然可以并发执行,但写入操作却必须单独执行了。在下图中演示的这个队列中,有多个读取操作,而且还有一个写入操作。
在这个并发队列中,读取操作是用普通的块来实现的,而写入操作则是用栅栏块来实现的
读取操作可以并行,但写入操作必须单独执行,因为它是栅栏块
实现代码很简单:
@property (nonatomic,strong) dispatch_queue_t syncQueue;
//_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/* 这里应该使用自己创建的并发队列,因为苹果文档中指出,如果使用的是全局队列或者创建的不是并发队列,
则dispatch_barrier_async实际上就相当于dispatch_async,就达不到我们想要的效果了 */
_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue", DISPATCH_QUEUE_CONCURRENT);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
测试一下性能,你就会发现,这种做法肯定比使用串行队列要快。其中,设置函数也可以改用同步栅栏块来实现,那样做可能会更高效,其原因之前已经解释过了——这里就要权衡拷贝块的时间和块执行时间了