GCD之同步锁和派发队列

你说的曾经没有我的故事 提交于 2019-12-07 15:19:44

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

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

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

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

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

_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方法,每次获取的结果却未必相同,在两次访问操作之间,其他线程可能会写入新的属性值.

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

用法如下:

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的效率会更高.因此我们无需担心加锁的事,只需把访问方法写好就行了.

        然后这还可以进一步进行优化, 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个有效方法>

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!