GCD之同步锁和派发队列

        在OC中,若是有多个线程要执行同一份代码,那么就可能会出现问题.(好比出现读写不一致的状况)这种状况下一般须要使用锁来实现某种同步机制.在GCD 出现以前,有两种办法,第一种是使用内置的同步块 synchronization block安全

- (void)synchronizedMethod {
    @synchronized(self) {
        //
    }
}

这种写法会根据给定的对象自动建立一个锁,并等到块中的代码执行完毕.执行到代码结尾处,锁就会自动释放. 并发

 须要注意的是:滥用@synchronized(self)会下降代码的执行效率,由于公用一个锁的那些同步块必须按照顺序执行.如果在self上频繁加锁,那么程序可能要等待另外一段无关的代码执行完毕才能继续执行当前的代码.异步

另外一方法是直接使用 NSLock对象.async

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod_lock {
    [_lock lock];
    // safe
    [_lock unlock];
}

    这两种方法都很好,可是也有缺陷.好比:极端状况下,同步块会致使死锁,并且性能也不见得高效,而若是直接使用锁对象的话,一旦赶上死锁将会很是难处理.函数

    替代方案就是使用GCD,他能以更简单 更高效的形式为代码加锁.  好比说属性就是开发者常常须要同步的地方,这种属性须要作成原子的 . 用atomic来修饰属性就能够实现这一点 .若是开发者想本身来编写访问方法的话,那么能够这样写:性能

- (NSString *)someString {
    @synchronized(self) {
        return _someString;
    }
}

- (void)setSomeString:(NSString *)someString {
    @synchronized(self) {
        _someString = someString;
    }
}

    刚才说过,滥用@synchronized(self) 会很危险,应为全部的同步块都会彼此抢夺同一个锁.要是有不少属性都这么写的话,那么每一个属性的同步块都要等到其余全部同步块执行完毕才能执行,这并非咱们想要的效果.咱们只是想令每一个属性各自独立的同步.另外,这么作只能提供必定程度上的线程安全,并不能保证绝对的线程安全. 固然,  访问属性的操做的确都是原子的. 使属性时,一定能从中获取到有效值,而后在同一个线程上屡次调用getter方法,每次获取的结果却未必相同,在两次访问操做之间,其余线程可能会写入新的属性值.测试

        有种简单而高效的办法能够代替同步块或者锁对象,那就是使用 "串行同步队列" .将要读取和写入的操做安排在一个同步队列里,便可保证数据同步.优化

用法以下:atom

dispatch_queue_t _syncQueue;
_syncQueue = dispathc_queue_create("syncQueue",NULL);
- (NSString *)someString {
    __block NSString *temp;
    dispatch_sync(_syncQueue, ^{
        temp = _otherString;
    });
    return temp;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_barrier_sync(_syncQueue, ^{
        _someString = someString;
    });
}

        此模式的思路是:把setter和getter都安排在同步队列里执行,如此一来,全部针对属性的访问操做就同步了. 全部加锁任务都在GCD中处理,而GCD是在至关深的底层实现的,相对于刚才提到的两种,GCD的效率会更高.所以咱们无需担忧加锁的事,只需把访问方法写好就好了.spa

        而后这还能够进一步进行优化, setter方法不必定非得是同步的 设置实例变量所用的块并无返回值,因此setter方法能够改为下面这样:

- (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

        此次只是把同步派发改为了异步派发,从调用者的角度来看,这个小改动能够提高设置方法执行速度,而读取操做与写入操做依然会按顺序执行. 可是这么改回带来另一个问题:若是你测试一下性能,可能会发现这种写法比原来慢,应为异步执行派发是,须要拷贝块.若拷贝块所用到的时间明显超过执行块所用到的时间,则这种作法将比原来慢. 因为这里的例子比较简单,因此这么改完以后可能会变慢. 如果派发给队列的块要执行的任务比较繁重时,那么仍然能够考虑这种备选方案.

    多个获取方法可并发执行,而getter方法和setter方法不能并发执行,利用这个特色能够写出更高效的代码. ---将setter方法和getter方法都放到同一个并发队列中,因为是并发队列,因此读写操做能够随时执行,而后咱们并不但愿这样. 这个问题用一个简单的GCD功能就能够解决,他就是栅栏.下面的函数能够向队列中派发块,将其做为栅栏使用

dispatch_barrier_async(dispatch_queue_t queue, ^{
        
    });
    dispatch_barrier_sync(dispatch_queue_t queue, ^{
        
    });

        在队列里,栅栏块必须单独执行,不能与其余块并发执行.这只对并发队列有意义,由于串行队列中的块老是按照顺序执行的. 并发队列若是发现接下来要执行的块是栅栏快,那么就要一直等当前全部的并发快都执行完毕,才会单独的执行这个栅栏块.等到栅栏快执行事后,在按照正常的方式继续向下执行.

        在下面的例子中 用栅栏快实现属性的设置方法.在setter方法中使用了栅栏块之后,对属性的读取操做依然能够并发执行,可是写入操做却必须单独执行了.

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString {
    __block NSString *temp;
    dispatch_sync(_syncQueue, ^{
        temp = _otherString;
    });
    return temp;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

   其执行示意图以下:

          |  读取 读取 读取

          |  读取 读取

          |  写入操做(栅栏块)

          |  读取 读取

          |  写入操做(栅栏块)

          |  读取 读取 读取

          ⬇️  读取 读取

 在这个并发队列中,读取操做使用普通的快来实现,而写入操做则是用栅栏块来实现的,读取操做能够并行,但写入操做必须单独执行,由于他是栅栏块.

 这种作法比使用串行队列要块.注意:设置函数也能够改用同步的栅栏块来实现,这样的话效率可能会更高,其缘由刚才已经解释过了.固然最好仍是根据实际状况选择最合适的方案.

    -->派发队列可用来表示同步语义,这用作法要比使用 @synchronized块或者NSLock对象更简单

    -->将同步和异步结合起来,能够实现与普通加锁机制同样的同步行为,而这么作却不会阻塞 执行异步派发的线程.

    -->使用同步队列及栅栏块,能够令同步行为更加高效.

 

参考书籍--<编写高质量iOS和OSX代码的52个有效方法>

相关文章
相关标签/搜索