引言
Grand Central Dispath(GCD)是苹果提供的强大工具,它几乎涵盖了多线程编程的所有方面。通过GCD,我们可以轻松地创建队列、管理线程,并以更优雅的方式处理并发任务。在前面的博客中,我们已经深入探讨了GCD的基本概念和常用方法,比如如何创建队列和管理线程。本篇博客我们将更进一步,探索GCD的一个高级功能——栅栏操作(Barrier),并了解它如何帮助我们在复杂的并发场景中实现更精细的任务控制。
栅栏操作的概念
GCD栅栏操作是一种在并发队列中控制任务执行顺序的机制。通过栅栏,我们可以确保在栅栏前提交的任务全部完成后,才会执行栅栏操作本身,且栅栏操作执行完毕后,才会继续执行栅栏后提交的任务。这样可以在并发环境中实现顺序控制,特别适用于某些任务之间需要严格前后关系的场景。
栅栏操作又分为两种,同步栅栏和异步栅栏。
同步栅栏(dispatch_barrier_sync)
同步栅栏是阻塞的,这意味着调用dispatch_barrier_sync的线程会一直等待,直到栅栏前的所有任务和栅栏操作本身都完成后,才能继续执行。
同步栅栏适用于需要立即确保栅栏操作完成后并获取结果的场景,比如栅栏操作中执行一个关键的计算,之后立即使用计算结果。
但是需要注意不要在主线程上使用dispatch_barrier_sync,因为它会阻塞当前线程,可能导致死锁或页面卡顿。
异步栅栏(dispatch_barrier_async)
异步栅栏不会阻塞调用它的现成。它会立即返回,并将栅栏操作放入队列中等待执行。栅栏操作会在栅栏前的任务完成后执行,但调用线程不会等待它的完成。
异步栅栏适用于需要确保栅栏前后的任务顺序,但不需要立即等待栅栏操作完成的场景。比如我们可以在栅栏操作中执行一些后台任务,而不需要阻塞主线程的操作。它可以有效地利用并发性能,而不影响调用线程的执行流。
栅栏的实现方式
下面我们就来看一下同步和异步栅栏的实现方式。
同步栅栏(dispatch_barrier_sync)
同步栅栏代码如下:
//MARK: 同步栅栏
func syncBarrier() {
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
concurrentQueue.async {
print("Task 1")
sleep(1)
}
concurrentQueue.async {
print("Task 2")
sleep(1)
}
// 同步栅栏操作
concurrentQueue.sync(flags: .barrier) {
print("Barrier Task")
sleep(1)
}
print("Task 3 Task 4提交到队列")
concurrentQueue.async {
print("Task 3")
sleep(1)
}
concurrentQueue.async {
print("Task 4")
sleep(1)
}
}
执行结果如下:
Task 1
Task 2
Barrier Task
Task 3 Task 4提交到队列
Task 3
Task 4
可以得出结论:
- 在执行Barrier Task之前,Task 1和Task 2会先被执行
- Barrier Task会在Task 1和Task 2后完成执行,并且在Barrier Task完成之前,Task 3和Task 4不会开始执行。
- 由于是同步栅栏,sync(flags: .barrier)会阻塞调用线程,直到Barrier Task执行完成。
异步栅栏(dispatch_barrier_async)
异步栅栏代码如下:
// MARK: 异步栅栏
func asyncBarrier() {
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
concurrentQueue.async {
print("Task 1")
sleep(1)
}
concurrentQueue.async {
print("Task 2")
sleep(1)
}
// 异步栅栏操作
concurrentQueue.async(flags: .barrier) {
print("Barrier Task")
sleep(1)
}
print("Task 3 Task 4提交到队列")
concurrentQueue.async {
print("Task 3")
sleep(1)
}
concurrentQueue.async {
print("Task 4")
sleep(1)
}
}
执行结果如下:
Task 3 Task 4提交到队列
Task 1
Task 2
Barrier Task
Task 3
Task 4
可以得出结论:
- Barrier Task仍然会在Task 1和Task 2之后执行,并确保在它执行完成之前不会有其他任务执行。
- 不同于同步栅栏,异步栅栏不会阻塞调用线程,所以代码会继续执行,任务会被正常提交到队列中。
栅栏的实践示例
虽然栅栏操作不像创建队列和管理线程那样广泛使用,但在某些特定的场景中,它仍然是不可或缺的工具。
数据整合
例如,在网络编程中,我们所需要的数据往往来自不同的服务器或者API。为了确保数据的一致性,我们可呢个需要从多个API获取数据,并在所有数据都处理完毕后,再将其进行整合、转换,最终进行本地缓存和UI更新。在这种情况下,栅栏操作可以确保这些步骤按顺序执行,从而避免数据不完整或者错误的情况发生。
示例代码如下:
//MARK: 使用栅栏 数据整合
func barrierOperation() {
let concurrentQueue = DispatchQueue(label: "com.example.fileQueue", attributes: .concurrent)
// 第一组任务:请求数据1
concurrentQueue.async {
print("request data 1")
// 模拟数据请求
sleep(1)
}
// 第一组任务:请求数据2
concurrentQueue.async {
print("request data 2")
// 模拟数据请求
sleep(1)
}
// 第一组任务:请求数据3
concurrentQueue.async {
print("request data 3")
// 模拟数据请求
sleep(1)
}
// 栅栏任务:处理读取的数据
concurrentQueue.async(flags: .barrier) {
print("Process and analyze data from both files")
}
// 第二组任务:将处理后的数据写回文件
concurrentQueue.async {
print("Write processed data to file")
}
// 第三组任务:其它操作
concurrentQueue.async {
print("Other operation")
}
}
读写锁
另一个典型的应用场景就是本地数据的存储和读取。为了提高程序的性能,通常我们只需要对写操作进行加锁,而读操作则可以并发进行。这是,栅栏操作可以帮助我们实现高效的读写锁,确保数据在多线程环境下的安全和一致性。
class ReadWriteLock: NSObject {
/// 数据
private var dataDict = [String: String]()
/// 并发队列
private var concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
//MARK: 异步 读
func readData(key: String) -> String? {
var result: String?
concurrentQueue.sync {
result = self.dataDict[key]
}
return result
}
//MARK: 写
func writeData(key: String, value: String) {
concurrentQueue.async(flags: .barrier) {
self.dataDict[key] = value
}
}
}
结语
在本文中,我们深入探讨了GCD栅栏操作的高级用法,通过实际示例展示了如何在并发队列中执行任务的顺序控制和实现读写锁。栅栏操作是处理复杂并发任务时非常强大的工具,它能够确保任务的顺序执行,避免数据不一致或冲突。
然而,在使用栅栏操作时,需要特别注意一些关键点。首先,尽量避免在全局并发队列中使用栅栏操作,因为全局并发队列通常用于系统级任务。栅栏操作的使用应局限于专门为特定任务创建的并发队列,这样可以更好地控制执行任务的顺序。
总之,理解并正确应用GCD栅栏可以帮助我们更有效地管理并发任务,提升应用的稳定性和性能。希望本篇博客能够为你的开发工作提供有价值的参考。