基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

dispatch_barrier_async (栅栏函数) 的作用和使用场景?

知识点图片

dispatch_barrier_async(栅栏函数)是 GCD(Grand Central Dispatch)中用于解决多线程并发读写安全问题的一个非常强大的工具。

简单来说,它就像一个交通管制的“栅栏”或“路障”,用来控制任务在并发队列中的执行顺序。

以下是关于它的详细解析:

1. 核心作用

在并发队列(Concurrent Queue)中,任务通常是并发执行的(乱序、同时进行)。dispatch_barrier_async 的作用是将队列中的任务在时间线上切分为三部分

  1. 栅栏前的任务:在栅栏任务加入队列之前提交的任务,会照常并发执行。
  2. 栅栏任务本身:当栅栏前的所有任务都执行完毕后,单独、排他地执行栅栏任务(此时队列暂时变成了串行)。
  3. 栅栏后的任务:只有当栅栏任务执行完毕后,后续提交的任务才会开始并发执行。

一句话总结: 它保证了在它执行的时候,队列中没有其他任务在执行(既没有读,也没有写)。


2. 执行流程图解

假设有一个并发队列,我们依次提交了任务:Read1, Read2, Read3, Barrier(Write), Read4, Read5

执行流程如下:

  1. Read1, Read2, Read3 并发执行(互不影响)。
  2. 等待 Read1, Read2, Read3 全部结束。
  3. 独占执行 Barrier(Write) 任务(此时 Read4, Read5 处于等待状态)。
  4. Barrier(Write) 结束。
  5. Read4, Read5 开始并发执行。

3. 最佳使用场景:多读单写(Reader-Writer Lock)

这是面试和实际开发中最常问到的场景。

问题背景:
为了提高效率,我们希望数据的读取(Read)是并发的(多个线程同时读没问题),但是数据的写入(Write)必须是互斥的(写的时候不能读,也不能有其他线程在写),否则会导致数据竞争(Race Condition)甚至 Crash。

解决方案:
使用自定义的并发队列 + dispatch_barrier_async

  • 读操作 (Getter): 使用 sync(同步)在并发队列中执行。因为队列是并发的,多个线程调用 Getter 可以同时运行。
  • 写操作 (Setter): 使用 barrier_async(栅栏异步)在同一并发队列中执行。这确保了写操作发生时,没有其他读写操作在进行。

Swift 代码示例

plaintext
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 (较少使用)

    • 将栅栏任务放入队列后,调用者线程被阻塞,直到栅栏任务执行完毕才继续。
    • 场景:你需要执行一个独占操作,并且必须等它做完才能进行下一步业务逻辑(比如初始化某些极其重要的资源)。

总结

  • 作用:分割并发队列的执行,实现“多读单写”的线程安全。
  • 核心行为:等待前序任务完成 -> 独占执行 -> 允许后续任务并发。
  • 关键点:必须配合自定义并发队列使用。
00:00
00:00