首页 > 代码库 > 多用派发队列,少用同步锁

多用派发队列,少用同步锁

今天晚上,第二遍阅读“多用派发队列,少用同步锁”,并且是一遍理解,一遍敲代码。阅读完这节之后,不明觉历。 

我就把我理解的,和作者所要表达的整理出来。(书名《编写高质量iOS与OS X代码的52个有效方法》)

在编码过程中,如果有多个线程要执行同一份代码,那么有时候会出现问题,比如set方法和get方法顺序错乱,就会导致输出结果不是自己期望的。

遇到这种情况 我讲列出几种解决方案,最后我会讲出最优质的代码。

1.采用内置的“同步块”(synchronization block)

- (void) synchronizedMethod

{

    @synchronized(self){

  //safe 处理代码    

    }

}

缺点:滥用@synchronized(self) 会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。若是self对象上频繁加锁,那么程序可能要等另一段于此无关的代码执行完毕,才能继续执行当前代码,这样做其实没有必要,另外在极端情况下同步块可能导致死锁。

 

2.使用 NSLock对象

  NSLock *_lock = [[NSLock alloc] init];

- (void) synchronizedMethod 

{

  

    [_lock lock];

  //safe 处理代码

    [_lock unlock];

}

//    NSRecursiveLock 也可以使用 递归锁。

缺点:效率不高,直接使用锁对象,一旦遇到死锁,就会非常麻烦。

 

-------------------GCD进入我们的视野------------------------

3.串行同步队列(serial synchroniation queue)

/*****

#ifdef __BLOCKS__

__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)

DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW

void

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

#endif

 

*****/

 

举一种常用的案例,实例变量的编写方法即set,get方法,将读取操作(get)和写入操作(set)都安排在同一个队列里,即可保证数据同步。

    dispatch_queue_t _syncQueue;

    NSString *_someString;

 

 

    _syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);// 串行队列

 

- (NSString *)someString

{

    __block NSString *localSomeString;

    dispatch_sync(_syncQueue, ^{

        localSomeString = _someString;

    });

    return localSomeString;

}

 

- (void)setSomeString:(NSString *)someString

{

    dispatch_sync(_syncQueue, ^{

        

        _someString = someString;

    });

}

 

此模式的思路是:把设置操作和获取操作都安排在 序列化 的队列里执行,这样的话,所有针对属性的访问操作都是同步了。全部加锁任务都在GCD中处理,我们无需担心,只要专心干别的事情就ok。

然而,设置方法并一定是非得是同步的,也就是说 设置方法可以改为 dispatch_async(_syncQueue, ^{  _someString = someString; });,改为dispatch_async,从调用者的角度来看,这个小的改动可以提升set方法到执行速度,然而set操作和get操作依然会按顺序执行,注意了,执行dispatch_async异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所花的时间,则这种做法还不如dispatch_sync同步.

 

4.并发队列

多个get方法可以可以并发执行,而set方法和get方法之间不能并发执行,利用这个特点,我们就可以大显 并发队列的神威了,其中我会给大家讲到一个神器--"栅栏或者叫阻断器",用来解决段前所讲的特点;

/****

#ifdef __BLOCKS__

__OSX_AVAILABLE_STARTING(__MAC_10_7,__IPHONE_4_3)

DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW

void

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

#endif

 

 

***/

 

 

    dispatch_queue_t  _globalSyncQueue; 

    _globalSyncQueue = 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

{

    /****代码1****/

    dispatch_async(_syncQueue, ^{

        _someString = someString;

    });

    

    /****代码1****/

 

    

    /****代码2****/

 

    //阻断器 or 栅栏

    dispatch_barrier_async(_globalSyncQueue, ^{

        _someString = someString;

    });

    // dispatch_barrier_sync 也可以尝试用用同步的栅栏块,可能效率更高效,理由“串行队列”已给出,“拷贝块花费时间”。。。

    /****代码2****/

 

}

 若只使用“代码1”所有的读取操作和写入操作都会在同一个队列上执行,不过由于是并发队列,所以set方法和get方法可以随时执行,而我们恰恰不想让这些操作随意执行。此问题用一个简单的GCD功能便可解决,它就是dispatch_barrier_async阻断器或者栅栏。

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

测试下性能,发现方法4比串行队列要快很多。

总结:

1.派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized块 或 NSLock对象更简单。

2.将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。

3.使用同步队列及栅栏块,可以令同步行为更加有效。

 

希望大家提出意见和建议进行分享与探讨。

 

 

转载请注明出处

 

多用派发队列,少用同步锁