理解 GCD 中 async 与 sync 的区别

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

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

csharp 复制代码
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属性包装器类似,即确保某些属性或值只能被同步修改,以避免多线程问题。

比如下面的代码:

swift 复制代码
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 中读取并写入它,这意味着我们最终将多次创建相同的日期格式化器,我们甚至可能遇到格式化器完全从缓存中丢失的情况。

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

总结

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

相关推荐
叽哥18 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa
00后程序员张3 天前
详细解析苹果iOS应用上架到App Store的完整步骤与指南
android·ios·小程序·https·uni-app·iphone·webview