前言
在 iOS 中,我们可以使用 Barrier 来对并发队列中的一个或者多个任务进行同步调度。也就是说,它允许你将线程不安全的对象转换为线程安全的对象。它为在并发调度队列中执行的代码块创建一个同步点。
派发 Barrier 任务是在处理并发队列时充当串行队列样式对象的一组函数。使用 GCD的 Barrier API 可以确保提交的任务是指定队列在特定时间内唯一执行的任务。
这意味着在调度 Barrier 任务之前提交到队列的所有任务必须在 Barrier 任务执行之前完成。当轮到 Barrier 任务时,GCD 确保队列在此期间不执行任何其他任务。当 Barrier 任务完成后,队列会返回到其默认实现。GCD 提供同步和异步 Barrier 函数。
代码示例
假设现在你有三项任务,需求是任务一和三的代码可以并发执行,任务二的项目需要串行执行,且任务二需要在任务一执行完再执行,任务三需要任务二执行完再执行。示例代码如下:
php
let concurrentQueue = DispatchQueue(label: "com.demo.dispatchBarrier", attributes: .concurrent)
for i in 1...5 {
concurrentQueue.async {
print("task 1, \(i)")
}
}
print("task 1 add complete")
for i in 1...5 {
concurrentQueue.async(flags: .barrier, execute: {
print("task 2, \(i)")
})
}
print("task 2 add complete")
for i in 1...5 {
concurrentQueue.async {
print("task 3,\(i)")
}
}
print("task 3 add complete")
上面代码的 log 如下:
csharp
task 1 add complete
task 1, 2
task 2 add complete
task 1, 1
task 2 add complete
task 1, 4
task 1, 3
task 1, 5
task 2, 1
task 2, 2
task 2, 3
task 2, 4
task 2, 5
task 3,1
task 3,3
task 3,4
task 3,5
task 3,2
由于三项任务我们都是异步添加到队列的,添加后会立即返回,不会阻塞主队列。所以看到三项任务的 add complete 在 task 1 还未执行完的时候就打印了。
根据任务执行的打印信息可以看到,任务一和任务三的执行顺序并不是升序的,这说明了任务一和任务三确实是并发执行的。而任务二的执行顺序是升序的,而且任务二的所有输出都是在任务一之后,任务三的所有输出在任务二之后。
注意事项:根据 Apple 的官方文档说明,如果你想使用 Barrier 的话,你指定的队列必须是你自己调用 dispatch_queue_create
创建的并发
队列。如果你指定串行队列或者 global 并行队列的话,其效果和 dispatch_async
函数效果一致。
使用场景
一般需要使用到 GCD Barrier 的场景就是多读单写:
- 可以允许读操作并发。
- 读操作和写操作同一时间只能执行一个。
- 写操作和写操作同一时间只能执行一个。
示例代码如下:
less
class ReadWriteLockByBarrier {
private let queue: DispatchQueue
init() {
queue = DispatchQueue(label: "com.demo.readWriteLock", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
}
deinit { }
func read<T>(_ closure: () -> T) -> T {
queue.sync {
closure()
}
}
func write(_ closure: @escaping () -> Void) {
queue.async(flags: .barrier) {
closure()
}
}
}
pthread_rwlock_rdlock / pthread_rwlock_wrlock
多读单写的技术实现方案还有另外一种,就是使用 pthread_rwlock_rdlock / pthread_rwlock_wrlock
来实现,具体示例代码如下:
scss
class ReadWriteLock {
private var rwlock = pthread_rwlock_t()
init() {
pthread_rwlock_init(&rwlock, nil)
}
deinit {
pthread_rwlock_destroy(&rwlock)
}
func read<T>(_ closure: () -> T) -> T {
pthread_rwlock_rdlock(&rwlock)
defer { pthread_rwlock_unlock(&rwlock) }
return closure()
}
func write(_ closure: () -> Void) {
pthread_rwlock_wrlock(&rwlock)
defer { pthread_rwlock_unlock(&rwlock) }
closure()
}
}
读写锁提供了一种有效的方法来处理对共享资源的并发访问。它们允许多个线程同时读取数据,同时确保一次只有一个线程可以写入资源。这会提高性能,因为读取操作不会阻塞其他读取操作,从而减少了线程之间的争用。
读写锁通常提供两种类型的锁:
- 读锁:这些锁可以由多个线程同时获取,允许它们同时读取共享资源。读锁不会阻塞其他读锁,但会阻止获取写锁。
- 写锁:这些锁提供对共享资源的独占访问,阻止其他线程获取读锁和写锁。写锁确保一次只有一个线程可以修改资源,从而防止数据不一致。
总结
- GCD Barrier 可以在并发队列中提供一个时机来执行同步任务。
- GCD Barrier 指定的队列必须是自己自定义的并发队列,否则效果和
dispatch_async
没有区别。 - 读写锁的两种实现方法:GCD Barrier 和 pthread_rwlock_rdlock / pthread_rwlock_wrlock。