dispatch_barrier_async (栅栏函数) 的作用和使用场景?
dispatch_barrier_async(栅栏函数)是 GCD(Grand Central Dispatch)中用于解决多线程并发读写安全问题的一个非常强大的工具。
简单来说,它就像一个交通管制的“栅栏”或“路障”,用来控制任务在并发队列中的执行顺序。
以下是关于它的详细解析:
1. 核心作用
在并发队列(Concurrent Queue)中,任务通常是并发执行的(乱序、同时进行)。dispatch_barrier_async 的作用是将队列中的任务在时间线上切分为三部分:
- 栅栏前的任务:在栅栏任务加入队列之前提交的任务,会照常并发执行。
- 栅栏任务本身:当栅栏前的所有任务都执行完毕后,单独、排他地执行栅栏任务(此时队列暂时变成了串行)。
- 栅栏后的任务:只有当栅栏任务执行完毕后,后续提交的任务才会开始并发执行。
一句话总结: 它保证了在它执行的时候,队列中没有其他任务在执行(既没有读,也没有写)。
2. 执行流程图解
假设有一个并发队列,我们依次提交了任务:Read1, Read2, Read3, Barrier(Write), Read4, Read5。
执行流程如下:
Read1,Read2,Read3并发执行(互不影响)。- 等待
Read1,Read2,Read3全部结束。 - 独占执行
Barrier(Write)任务(此时Read4,Read5处于等待状态)。 Barrier(Write)结束。Read4,Read5开始并发执行。
3. 最佳使用场景:多读单写(Reader-Writer Lock)
这是面试和实际开发中最常问到的场景。
问题背景:
为了提高效率,我们希望数据的读取(Read)是并发的(多个线程同时读没问题),但是数据的写入(Write)必须是互斥的(写的时候不能读,也不能有其他线程在写),否则会导致数据竞争(Race Condition)甚至 Crash。
解决方案:
使用自定义的并发队列 + dispatch_barrier_async。
- 读操作 (Getter): 使用
sync(同步)在并发队列中执行。因为队列是并发的,多个线程调用 Getter 可以同时运行。 - 写操作 (Setter): 使用
barrier_async(栅栏异步)在同一并发队列中执行。这确保了写操作发生时,没有其他读写操作在进行。
Swift 代码示例
class ThreadSafeArray<T> {
private var array = [T]()
// 1. 必须创建自定义的并发队列 (attributes: .concurrent)
private let queue = DispatchQueue(label: "com.my.concurrentQueue", attributes: .concurrent)
// 读操作:并发
func read(at index: Int) -> T? {
// 使用 sync 是为了立刻拿到返回值
// 因为是并发队列,多个线程可以同时进入这里读取
return queue.sync {
if index >= 0 && index < array.count {
return array[index]
}
return nil
}
}
// 写操作:独占(栅栏)
func append(_ element: T) {
// 使用 barrier,确保此时只有这一个任务在操作数组
// 使用 async 是为了不阻塞调用者的线程(通常写操作不需要立刻返回值)
queue.async(flags: .barrier) {
self.array.append(element)
}
}
}
4. 重要注意事项
A. 必须使用自定义并发队列
dispatch_barrier_async 只有在自定义的并发队列(DispatchQueue(label: ..., attributes: .concurrent))上才有效。
- 如果用在串行队列(Serial Queue): 没意义,因为串行队列本身就是从头到尾一个接一个执行,本身就是天然的栅栏。
- 如果用在全局并发队列(Global Queue): 系统为了性能,不会因为你提交了一个栅栏任务就阻塞全局队列中的其他系统任务。在全局队列中,它退化为普通的
async,失去了栅栏的阻塞效果。
B. 死锁风险
在实现 Getter 方法时,通常使用 queue.sync。千万不要在 queue.sync 的闭包里再调用同一个队列的 sync 任务,否则会死锁(虽然这是 GCD 的通用规则,但在封装线程安全对象时容易犯错)。
5. dispatch_barrier_async vs dispatch_barrier_sync
虽然两者都具有“栅栏”的特性(独占执行),但区别在于提交任务的线程是否需要等待:
dispatch_barrier_async(推荐用于写操作):- 将栅栏任务放入队列后,立即返回,调用者线程继续往下跑。
- 栅栏任务会在未来某个时刻独占执行。
- 场景:Setter 方法,更新数据,不需要立刻拿到结果。
dispatch_barrier_sync(较少使用):- 将栅栏任务放入队列后,调用者线程被阻塞,直到栅栏任务执行完毕才继续。
- 场景:你需要执行一个独占操作,并且必须等它做完才能进行下一步业务逻辑(比如初始化某些极其重要的资源)。
总结
- 作用:分割并发队列的执行,实现“多读单写”的线程安全。
- 核心行为:等待前序任务完成 -> 独占执行 -> 允许后续任务并发。
- 关键点:必须配合自定义并发队列使用。