GCD之dispatch_async;dispatch_sync
一、概念与类型
对于GCD来说,所有的执行都放到队列中(queue),队列的特点是FIFO(先提交的先执行)。
GCD的队列分为几种,主队列(main),全局队列(global),用户创建队列(create)
对于全局队列,默认有四个,分为四个优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
DISPATCH_QUEUE_PRIORITY_HIGH :优先级最高,在default,和low之前执行
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认优先级,在low之前,在high之后
DISPATCH_QUEUE_PRIORITY_LOW 在high和default后执行
DISPATCH_QUEUE_PRIORITY_BACKGROUND:提交到这个队列的任务会在high优先级的任务和已经提交到background队列的执行完后执行。官方文档:(the queue is scheduled for execution after all high priority queues have been scheduled and the system runs items on a thread whose priority is set for background status.)
二、dispatch_async与dispatch_sync
- dispatch_sync
提交到队列中同步执行 - dispatch_async
提交到队列中异步执行,立即返回 - dispatch_barrier_sync
- dispatch_barrier_async
在队列中,栅栏块必须单独执行,不能与其它块并行。这只对队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列中如果发现接下来要处理的块是个栅栏块(barrier block),那么就一直等到当前所有并发块都执行完毕,才会单独执行这个栅栏块。
这是Effective Objective-C 2.0 这本书 中对dispatch_barrier_async的说明,书中有一段示例代码,大概是这样的:
- (instancetype)init
{
self = [super init];
if (!self) {
return nil;
}
_concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
return self;
}
- (NSString *)name
{
__block NSString *name;
dispatch_sync(_concurrentQueue, ^{
name = _name;
});
return name;
}
- (void)setName:(NSString *)name
{
dispatch_barrier_async(_concurrentQueue, ^{
_name = name;
});
}
问题来了,以上代码能实现对name属性的读写同步吗?
按道理,是可以的。但事实并不可以,因为在这里苹果给我们挖了一个坑。。。
原因就是 在这里 我们使用的是全局并发队列。。
都是 并发队列 凭啥就不一样呢??
就是说,栅栏块应该在自己创建的并行队列里执行,如果是在串行队列或是全局并行队列中执行,那么就起不到栅栏的作用,和dispatch_async 函数效果一样了。
所以上面代码只要把_concurrentQueue 改成自己创建的
_concurrentQueue = dispatch_queue_create("com.people.test", DISPATCH_QUEUE_CONCURRENT);
就可以实现 读写的同步了。
隔离队列
再来理解一个隔离队列的概念,也是跟dispatch_barrier_sync和dispatch_barrier_async有关。
例如操作系统中的经典的读者-写者问题,简而言之,我们可以在同一时刻有多个读者,但同一时刻只能有一个线程可以写入。解决方法就是利用GCD的四种 APIs
- dispatch_sync
- dispatch_async
- dispatch_barrier_sync
- dispatch_barrier_async
我们的想法是读操作可以支持同步和异步,而写操作也能支持异步写入,并且必须确保是写入的是同一个引用。GCD 的 barrier 集合 APIs 提供了解决方案:他们将会一直等到队列中的任务清空,才会继续执行 block。使用 barrier APIs 可以用来限制我们对字典对象的写入,并且确保我们不会在同一时刻进行多个写操作,以及正在写操作时同时进行读操作。看一段代码:
class IdentityMap<T: Identifiable> {
var dictionary = Dictionary<String, T>()
let accessQueue = dispatch_queue_create("com.khanlou.isolation.queue", DISPATCH_QUEUE_CONCURRENT)
func object(withID ID: String) -> T? {
var result: T? = nil
dispatch_sync(accessQueue, {
result = dictionary[ID] as T?
})
return result
}
func addObject(object: T) {
dispatch_barrier_async(accessQueue, {
dictionary[object.ID] = object
})
}
}
dispatch_sync 将会分发 block 到我们的隔离队列上然后等待其执行完毕。通过这种方式,我们就实现了同步读操作(如果我们搞成异步读取,getter 方法就需要一个 completion block)。因为 accessQueue 是并发队列,这些同步读取操作可以并发执行,也就是允许同时读。
dispatch_barrier_async 将分发 block 到隔离队列上,async 异步部分意味着会立即返回,并不会等待 block 执行完毕。这对性能是有好处的,但是在一个写操作后立即执行一个读操作会导致读到一个半成品的数据(因为可能写操作还未完成就开始读了)。
dispatch_barrier_async 中的 barrier 部分意味着:只要 barrier block 进入队列,并不会立即执行,而是会等待该队列其他 block 执行完毕后再执行。所以在这点上就保证了我们的 barrier block 每次都只有它自己在执行。而所有在他之后提交的 block 也会一直等待这个 barrier block 执行完再执行。
传入 dispatch_barrier_async() 函数的 queue,必须是用 dispatch_queue_create 创建的并发 queue。如果是串行 queue 或者是 global concurrent queues,这个函数就会变成 dispatch_async() 了