何时使用 GCD 队列以及您何时知道需要它们? Swift

When are GCD queues used and when do you know you need them? Swift

阅读并发和串行队列、同步和异步后,我想我对如何创建队列以及它们的执行顺序有了一个想法。我的问题是在我看到的任何教程中,none 他们实际上告诉你很多用例。例如:

我有一个网络管理器,它使用 URLSessions 并序列化 json 向我的 api 发送请求。将其包装在 .utility 队列或 .userInitiated 中是否有意义,或者我只是不将其包装在队列中。

let task = LoginTask(username: username, password: password)

let networkQueue = DispatchQueue(label: "com.messenger.network", 
qos: DispatchQoS.userInitiated)

networkQueue.async {
    task.dataTask(in: dispatcher) { (user, httpCode, error) in
        self.presenter?.loginUserResponse(user: user, httpCode: httpCode, error: error)
    }
}

我的问题是:是否有任何我可以遵循的指南来了解何时需要使用队列,因为我无法在任何地方找到此信息。我意识到苹果提供了示例用法,但它非常模糊

调度队列在许多用例中使用,因此很难一一列举,但两个非常常见的用例如下:

  1. 您有一些昂贵的 and/or 耗时进程,您想 运行 在当前线程以外的某个线程上执行。当你在主线程上并且你想 运行 在后台线程上做一些事情时,通常会使用它。

    一个很好的例子就是图像处理,这是一个众所周知的计算(和内存)密集型过程。因此,您将创建一个图像处理队列,然后将每个图像处理任务分派到该队列。您还可以在 UI 更新完成后分派回主队列(因为所有 UI 更新都必须在主线程上发生)。一个常见的模式是:

    imageQueue.async {
        // manipulate the image here
    
        // when done, update the UI:
    
        DispatchQueue.main.async {
            // update the UI and/or model objects on the main thread
        }
    }
    
  2. 你有一些共享资源(它可以是一个简单的变量,它可以是与一些其他共享资源如文件或数据库的交互),无论从哪个线程同步调用它。这通常是使本质上不是线程安全的东西以线程安全的方式运行的更广泛策略的一部分。

调度队列的优点在于它大大简化了多线程代码的编写,这是一项非常复杂的技术。

问题是你的例子,发起一个网络请求,已经 运行 后台线程上的请求并且 URLSession 为你管理所有这些,所以使用队列没有什么价值为此。


为了全面披露,令人惊讶的是,除了上面讨论的基本调度队列之外,还有各种直接(例如调度组或调度源)或间接(例如操作队列)使用 GCD 的工具:

  • 调度组:有时您会启动一系列异步任务,并且希望在它们全部完成时收到通知。您可以使用调度组(随机示例参见 )。这使您无需跟踪所有这些任务何时自己完成。

  • Dispatch "apply"(现在称为 concurrentPerform):有时当您 运行 执行一些大规模并行任务时,您希望使用尽可能多的线程你合理地可以。所以 concurrentPerform 让你可以有效地并行执行 for 循环,Apple 已经针对你的特定设备的内核和 CPU 数量对其进行了优化,同时不会在任何时候让太多并发任务淹没它,耗尽有限数量的工作线程。有关 运行 并行 for 循环的示例,请参见

  • 调度来源:

    • 例如,如果您有一些正在做大量工作的后台任务并且您想要更新 UI 进度,有时这些 UI 更新可以来得比 UI 处理得更快。因此,您可以使用调度源(DispatchSourceUserDataAdd)将后台进程与 UI 更新分离。示例参见前面提到的

    • 传统上,主 运行 循环上的 Timer 运行s。但有时你想在后台线程上 运行 它,但是用 Timer 做到这一点很复杂。但是您可以使用 DispatchSourceTimer(GCD 计时器)到 运行 主队列以外的队列上的计时器。有关如何创建和使用调度计时器的示例,请参阅 。调度计时器也可用于避免某些基于 targetTimer 对象很容易引入的强引用循环。

  • 障碍:有时在使用并发队列时,您希望大多数事情 运行 同时进行,但对于其他事情 运行 串行相对于队列中的其他所有事情.障碍是表达 "add this task to the queue, but make sure it doesn't run concurrently with respect to anything else on that queue."

    的一种方式

    屏障的一个例子是 reader-writer 模式,其中从某些内存资源读取可以与所有其他读取同时发生,但任何写入都不能与其他任何内容同时发生队列。见 or .

  • Dispatch 信号量:有时您需要让两个任务 运行ning 在不同的线程上相互通信。您可以将一个线程的信号量用于另一个线程的 "wait" "signal"。

    信号量的一个常见应用是使本质上异步的任务以更同步的方式运行。

    networkQueue.async {
        let semaphore = DispatchSemaphore(0)
        let task = session.dataTask(with: url) { data, _, error in
            // process the response
    
            // when done, signal that we're done
            semaphore.signal()
        }
        task.resume()
        semaphore.wait(timeout: .distantFuture)
    }
    

    这种方法的优点是在异步网络请求完成之前,分派的任务不会完成。因此,如果您需要发出一系列网络请求,但不是同时 运行,信号量可以完成。

    不过,应谨慎使用信号量,因为它们本质上效率低下(通常会阻塞一个线程等待另一个线程)。另外,请确保您永远不会 wait 从主线程获取信号量(因为您正在破坏拥有异步任务的目的)。这就是为什么在上面的例子中,我在等待 networkQueue,而不是主队列。综上所述,通常有比信号量更好的技术,但它有时很有用。

  • 操作队列:操作队列建立在 GCD 调度队列之上,但提供了一些有趣的优势,包括:

    • 能够在自定义 Operation 子类中包装固有的异步任务。 (这避免了我之前讨论过的信号量技术的缺点。)当 运行 在后台线程上执行本质上同步的任务时,通常使用调度队列,但有时您想要管理一堆本身是异步的任务.一个常见的例子是在 Operation 子类中包装异步网络请求。

    • 轻松控制并发度的能力。调度队列可以是串行的也可以是并发的,但是设计控制机制很麻烦,例如,说 "run the queued tasks concurrent with respect to each other, but no more than four at any given time." 操作队列使用 maxConcurrentOperationCount 使这变得容易得多。 (有关示例,请参阅 。)

    • 在各种任务之间建立依赖关系的能力(例如,您可能有一个用于下载图像的队列和另一个用于处理图像的队列)。使用操作队列,您可以有一个操作用于下载图像,另一个操作用于图像处理,并且您可以使后者依赖于前者的完成。


还有很多其他与 GCD 相关的应用程序和技术,但这些是我经常使用的一些应用程序和技术。