理解 GCD 中 async 与 sync 的区别

2,196 阅读5分钟

在我们编写 iOS 代码的时候,经常会碰到异步执行的代码。有时你知道你正在编写一些异步执行的代码,有时则直接传递一个 completion handler,这个 handler 可能会也可能不会在不同的队列异步执行。

如果你对 DispatchQueue 很熟悉的话,你可能会经常写下面的代码:

DispatchQueue.main.async {
  // 执行你自己的逻辑
}

DispatchQueue 不光有 async,它还有一个 sync。本篇文章主要聚焦这两个方法,来讲解一下它俩的不同之处。

DispatchQueue.async

每个 DispatchQueue 实例对象都可以调用 async 方法。不管是主队列 DispatchQueue.main、全局队列 DispatchQueue.global() 还是你自己创建的。它的作用是它接收到的闭包中的代码会稍后执行(异步执行)。

当你调用队列的异步方法时,就代表你要求它执行闭包中的工作,但不需要立马执行该工作,或者更确切地说,你不想异步方法的任务阻塞现有的任务。

想象一下在餐馆当服务员的情景。你的工作是接受客人点的菜,并把它们转达给厨房。每次你为客人点餐,你都会走到厨房,递给他们一张纸条,上面写着他们需要准备的菜,然后你继续下一道菜。最后,厨房会通知你,他们已经完成了一份订单,你可以拿起订单,把它送到客人的桌子上。

在这个类比中,服务员可以被认为是它自己的调度队列。你可以将其视为主队列,因为如果服务员被阻塞,则不会再接受订单,并且餐厅会陷入停顿(假设这是一家只有一个服务员的小餐厅)。厨房可以被看作是一个不同的调度队列,服务员每次请求下一个订单时,都会异步调用这个队列。

作为一名服务员,你要完成工作,然后继续做下一项任务。因为你异步调度到厨房,没有人会被阻塞,每个人都可以执行他们的工作。

上面的类比解释了从一个队列到另一个队列的异步调用,但它没有解释从同一队列内部的异步调度。

例如,当你已经在主队列中时,没有什么可以阻止你调用 DispatchQueue.main.async。那么这是怎么回事呢?

非常相似,真的。当您在同一队列内异步调度时,应该执行的工作体将在队列当前正在执行的工作之后执行。

回到服务员的比喻,如果你走过一张桌子,告诉他们“我马上就来帮你点单”,而你现在正在给另一张桌子送饮料,你实际上是在异步调度自己。你已经安排了一大堆工作要做,但你也不想阻碍你现在正在做的事情。

总而言之,DispatchQueue.async 允许你使用闭包来安排要完成的工作,而且不会阻塞任何正在进行的工作。在大多数情况下,当你需要分派任务到队列时,会希望使用 async。然而,在一些场景下,DispatchQueue.sync 也是同样重要的。

DispatchQueue.sync

由异步替换为同步是非常简单的,只需将 async 改为 sync 即可。但如果你不太熟悉队列的同步的话,你可能认为即使你在同步调度,你也不会阻塞你所在的队列,因为工作在另一个队列上运行。

不幸的是,这并不完全正确。让我们回到上一节的餐厅。

我解释了服务员是如何异步地把饭菜准备工作送到厨房的。这使得服务员可以继续为客人点单和送餐。

现在想象一下,服务员向厨房要点菜,然后站在那里等待。什么都不做。服务员并不是因为他们自己的工作而受阻,而是因为他们必须等待厨师准备好他们要的菜。

这就是DispatchQueue 的同步。当你使用同步调度工作任务时,当前队列将等待工作任务完成,以便它可以继续执行接下来的任何工作。

到目前为止,我使用DispatchQueue.sync的最常见的情况与 @Atomic属性包装器类似,即确保某些属性或值只能被同步修改,以避免多线程问题。

比如下面的代码:

class DateFormatterCache {
  private var formatters = [String: DateFormatter]()
  private let queue = DispatchQueue(label: "DateFormatterCache: \(UUID().uuidString)")

  func formatter(using format: String) -> DateFormatter {
    return queue.sync { [unowned self] in
      if let formatter = self.formatters[format] {
        return formatter
      }

      let formatter = DateFormatter()
      formatter.locale = Locale(identifier: "en_US_POSIX")
      formatter.dateFormat = format
      self.formatters[format] = formatter

      return formatter
    }
  }
}

每次 DateFormatterCache的实例调用 formatter(using:)函数时,该工作都会同步分配到特定的队列。默认情况下,调度队列是串行的,这意味着它们按照任务调度的顺序一个接一个地执行每个任务。

这意味着我们确定一次只有一个任务访问 formatters,从中读取,并在需要时缓存一个新的 formatter。

如果我们同时从多个线程调用 formatter(using:) ,我们确实需要这种同步行为。如果我们不这样做,多个线程将从 formatters 中读取并写入它,这意味着我们最终将多次创建相同的日期格式化器,我们甚至可能遇到格式化器完全从缓存中丢失的情况。

如果你喜欢用餐馆的比喻来理解,可以把它想象成餐馆里的所有客人都能在一张纸上写下他们点的菜。服务员只允许将一张纸递给厨房,而这张纸必须包含所有的订单。每次客人向服务员要点餐单时,服务员都会给客人一份目前已知的点餐单。

总结

请记住,在将工作分派到队列时,异步通常是你想要的方法,而当你希望具有原子操作并确保字典、数组等的线程安全时,同步是非常重要的。