Dispatch Queues 技术实践

652 阅读6分钟

简介

Grand Central Dispatch (GCD) 是 Apple 提供的一个多核编程优化技术,用于简化并发编程,提高应用的性能和响应速度。GCD 提供了一套高效的 C 语言 API,用于管理和调度任务,并充分利用多核处理器的能力。GCD 通过抽象和封装并发编程的复杂性,提供了一种更高效、更易用的并发编程模型。

调度队列

调度队列是一种在应用程序中异步和并发执行任务的简便方法。任务是应用程序需要执行的一些工作。例如,你可以定义一个任务来执行一些计算,创建或修改数据结构,处理从文件中读取的数据,或进行其他操作。你可以通过将相应的代码放在函数或块对象内,然后将其添加到调度队列中来定义任务。

调度队列是一种类似对象的结构,用于管理提交给它的任务。所有调度队列都是先进先出(FIFO)数据结构。因此,你添加到队列中的任务总是按照添加的顺序开始执行。GCD 为你自动提供了一些调度队列,但你也可以为特定目的创建其他调度队列。

  • 主队列 (串行队列)

    向主队列的提交的任务运行在主线程。注意会造成死锁的情况。一般用来更新UI

    DispatchQueue.main
    // 可以在子线程调用`async`更新UI
    DispatchQueue.main.async {
    }
    
  • 串行队列

    串行队列按添加到队列的顺序一次执行一个任务。当前正在执行的任务运行在一个独立的线程上(每个任务的线程可能不同),该线程由调度队列管理。串行队列通常用于同步访问特定资源。

    // 主队列也是一种串行队列
    DispatchQueue.main
    // 自定义串行队列
    let serialQueue = DispatchQueue(label: "serial")
    
  • 并发队列

    并发队列(也称为全局调度队列的一种类型)可以同时执行一个或多个任务,但这些任务仍然按照它们被添加到队列的顺序开始执行。当前正在执行的任务运行在由调度队列管理的不同线程上。在任何给定时间点上执行的确切任务数量是可变的,并且取决于系统条件。

    // 全局并发队列
    DispatchQueue.global()
    // 自定义并发队列
    let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
    

添加任务到队列

  • 异步执行

    在可能的情况下,优先使用async函数进行异步执行,而不是同步执行的替代方案。当你将一个块对象或函数添加到队列时,无法确切知道代码将何时执行。因此,通过异步方式添加块或函数可以安排代码的执行,并允许从调用线程继续执行其他工作。如果你是在应用程序的主线程中调度任务(例如响应某个用户事件),这一点尤其重要。

  • 同步执行

    尽管应尽可能使用异步方式添加任务,但仍有时候你可能需要同步方式添加任务,以防止竞争条件或其他同步错误。在这些情况下,你可以使用 sync 函数将任务添加到队列中。这些函数会阻塞当前的执行线程,直到指定的任务执行完成。

  • 重要提示

    永远不应该在同一个队列中的任务中调用 sync 函数。这一点对于串行队列尤为重要,因为会导致死锁,但对并发队列也应避免使用。

    //死锁1: 在主线程环境中调用主队列sync
    DispatchQuue.main.sync {}
    ​
    //死锁2:同步队列调用sync
    let serialQueue = DispatchQueue(label: "seraial")
    //Task 1想要执行完成,必须等待Task 2 执行完成,然而Task 2要执行完成,必须等待task1执行完成。
    // 外层改成async,也会死锁
    serialQueue.sync {
        print("Task 1 start")
        serialQueue.sync {
            print("Task 2 Finished")
        }
        print("Task 1 Finished")
    }
    

其它技术

  • Dispatch Group

Dispatch groups 是一种阻塞线程直到一个或多个任务完成执行的方法。你可以在必须等待所有指定任务完成后才能继续进行的地方使用这种行为。例如,在调度多个任务来计算一些数据之后,你可以使用一个 group 来等待这些任务完成,然后处理它们的结果。使用 dispatch groups 的另一种方法是作为线程合并的替代方案。与其启动多个子线程然后逐一合并,不如将相应的任务添加到一个 dispatch group 中,并等待整个 group 完成。

// 例子1 - 阻塞当前线程
let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
    print("Task1 - Start")
    sleep(2)
    print("Task1 - End")
}

queue.async(group: group) {
    print("Task2 - Start")
    sleep(1)
    print("Task2 - End")
}

//阻塞当前线程
group.wait(timeout: .distantFuture)
// 例子2 不阻塞当前线程
let group = DispatchGroup()
let queue = DispatchQueue.global()

for i in 0..<5 {
    group.enter()
    queue.async {
        print("Task \(i) is starting.")
        sleep(2) // 模拟工作
        print("Task \(i) has finished.")
        group.leave()
    }
}

//不会阻塞当前线程, 所有提交到group中任务完成执行block体
group.notify(queue: DispatchQueue.main) {
    print("All tasks have finished.")
}
print("Waiting for tasks to complete...")

  • Dispatch Barrier

当工作项提交到并发队列时,带有barrier标志的工作项将会作为栅栏工作项,提交在栅栏工作项之前的工作项全部执行完毕,然后栅栏工作项才开始执行,一旦执行完毕,队列将继续调度在栅栏工作项之后提交的工作项。

// 共享资源
var sharedArray = [Int]()
// 并发队列
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
// 并发读数组
func readArray() {
    concurrentQueue.async {
        print("Read ...")
        Thread.sleep(forTimeInterval: 2.0)
        let arrayCopy = sharedArray
        print("Read array: \(arrayCopy)")
    }
}
// 使用barrier同步写数组,避免Race Condition
func writeArray(item: Int) {
    concurrentQueue.async(flags: .barrier) {
        print("write \(item) ...")
        Thread.sleep(forTimeInterval: 2.0)
        sharedArray.append(item)
        print("write success")
    }
}

// 使用`DispatchGroup`测试读写数组
let group = DispatchGroup()

for _ in 0..<6 {
    group.enter()
    concurrentQueue.async {
        readArray()
        group.leave()
    }
}

for i in 0..<6 {
    group.enter()
    concurrentQueue.async {
        writeArray(item: i)
        group.leave()
    }
}
// 所有任务执行完通知
group.notify(queue: concurrentQueue) {
    print("All tasks finished, sharedArray: \(sharedArray)")
}
  • Dispatch Semaphore

信号量是一种用于线程同步的机制,广泛应用于多线程编程中。它通过维护一个计数器来控制对共享资源的访问,确保在多线程环境数据一致性和防止资源竞争。

  1. 线程同步
var sharedResource = 0
let queue = DispatchQueue.global()
// 初始化信号量计数器为 1,保证只有一个线程能访问共享数据
let semaphore = DispatchSemaphore(value: 1)
for i in 1...5 {
    queue.async {
        //信号量计数器减少1,当<0时阻塞线程,防止有两个线程同时访问共享数据
        semaphore.wait()
        let oldValue = sharedResource
        print("Old value: \(oldValue)")
        sharedResource += i
        Thread.sleep(forTimeInterval: 1.0)
        print("Increment \(i), Updated value \(sharedResource)")
        semaphore.signal()
    }
}
  1. 控制并发线程数
var sharedArray = [Int]()

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)

// 控制线程的数量
let semaphore = DispatchSemaphore(value: 3)

func performTask(taskNumber: Int) {
    // 信号量的计数值 -1,当信号量的计数值 <0 时,阻塞线程
    semaphore.wait()
    print("Task \(taskNumber) started \(Thread.current)")
    Thread.sleep(forTimeInterval: 2.0)
    print("Task \(taskNumber) completed \(Thread.current)")
    // 信号量的计数值 +1
    semaphore.signal()
}

for i in 0..<12 {
    concurrentQueue.async {
        performTask(taskNumber: i)
    }
}

结语

Dispatch Queues 是 iOS 和 macOS 开发中不可或缺的核心技术之一,通过合理地利用它,可以编写出高效、稳定的应用程序。希望本次分享能够帮助大家更好地理解和应用 Dispatch Queues,在实际开发中发挥其强大的作用。

如果你有任何问题或者进一步的讨论,请评论区留言