4

Core Data 改革:实现 SwiftData 般的优雅并发操作

 2 weeks ago
source link: https://fatbobman.com/zh/posts/core-data-reform-achieving-elegant-concurrency-operations-like-swiftdata/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Core Data 改革:实现 SwiftData 般的优雅并发操作

东坡肘子

发表于 2024 年 4 月 18 日

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

SwiftData,作为 Core Data 的后继者,引入了众多创新和现代化的设计思想。尽管它已经推出一段时间,但许多开发者还未在他们的项目中采用。这种状况部分是因为 SwiftData 对操作系统版本的要求较高,另一方面,由于 SwiftData 在某些功能方面还不够成熟,即便操作系统版本符合要求,开发者也可能因为功能限制而选择继续使用 Core Data。我们是否能将 SwiftData 中的一些卓越设计理念和巧妙实现,融合到 Core Data 的实际使用中呢?本文旨在探讨如何在 Core Data 中引入类似 SwiftData 的优雅和安全的并发操作,以实现一个 @ModelActor 的 Core Data 版本。

perform VS @ModelActor

尽管在理论上,只需要遵循一个简单原则即可在 Core Data 中安全地进行并发操作:托管对象应仅在其绑定的托管对象上下文及相应线程中被操作。然而,要遵守这个规则,完全取决于开发者的耐心和经验,而编译器在这方面无法提供帮助。因此,在 Core Data 的并发代码实践中,广泛使用基于上下文的 perform 方法,这种做法既繁琐又难以控制。

SwiftData 克服了这些障碍。通过采用 Swift 的现代并发模型,开发者可以避开 perform,将数据操作逻辑封装在一个 Actor 中。此外,SwiftData 还引入了 @ModelActor 宏,允许 Actor 在特定线程中执行,为开发者提供了一种优雅、安全、高效的并发操作方式。

Swift
@ModelActor
actor DataHandler {
    func updateItem(identifier: PersistentIdentifier, timestamp: Date) throws {
        guard let item = self[identifier, as: Item.self] else {
            throw MyError.objectNotExist
        }
        item.timestamp = timestamp
        try modelContext.save()
    }
}

推荐阅读 关于 Core Data 并发编程的几点提示 了解更多关于 Core Data 并发操作的建议。同时,浏览 SwiftData 中的并发编程 可以深入了解 SwiftData 在并发操作方面的创新之处。

自定义 Actor 执行者

自 Swift 5.5 引入新并发模型以来,Actor 已经成为开发者执行串行操作的首选机制。然而,这种新的并发设计有意识的模糊了代码的实际运行方式和细节,使得很长一段时间,开发者都无法决定 Actor 的具体执行位置( 也就是所在的线程 )。

遵循 Core Data 并发操作的基本原则,所有对托管对象的操作都必须在其所属上下文的线程上执行。这个限制意味着我们无法直接将 Actor 模型应用于 Core Data 的并发操作中。

然而,Swift 社区通过 SE-392 提案,提出了自定义 Actor 执行者的概念,并在 Swift 5.9 中实现了这一功能。SwiftData 利用这一新特性,为开发者提供了一种全新的并发开发体验。

这意味着,我们现在可以为 Actor 创建一个 Executor,用它来替换 Actor 默认的任务调度机制。

创建自定义 Executor

在构建自定义 Actor 执行者之前,了解一些基本概念是必要的:

  • Executors 协议:一个基本的执行器,不提供任何调度顺序的保证,可以并行或串行地执行提交的任务。
  • SerialExecutor 协议:一个串行执行器,符合 Executors 协议。它保证任务的互斥执行,也就是说一次只能执行一个任务。这个协议被 Actor 用来实现他们的串行执行语义。
  • UnownedSerialExecutor:一个优化过的 SerialExecutor 引用类型,它为 Swift 并发运行时提供了高效的执行器引用机制,从而避免不必要的开销。这有助于提升 Swift 并发编程的性能。
  • ExecutorJob:一个可以被执行的任务类型,支持 Sendable 协议且不可复制(@noncopyable)。执行者在需要执行任务时,会调用 ExecutorJob.runSynchronously(on:) 方法,该方法会消耗掉 ExecutorJob 实例,并在指定的执行者上同步执行任务。
  • UnownedExecutorJob:作为 ExecutorJob 的补充类型,它是可复制的,使得任务存储和传递变得更加容易。

Actor 构建自定义执行者时,大致步骤如下:

  • 声明一个遵循 SerialExecutor 协议的类型。
  • 在其内部实现一个可以进行串行操作的机制。
  • enqueue 方法中,将 ExecutorJob 转换为 UnownedExecutorJob 并提交给串行机制执行。

具体的实现示例如下所示:

Swift
public final class CustomExecutor: SerialExecutor {
  // 串行工具
  private let serialQueue: DispatchQueue
  public init(serialQueue: DispatchQueue) {
    self.serialQueue = serialQueue
  }

  public func enqueue(_ job: consuming ExecutorJob) {
    // 转换 ExecutorJob 为 UnownedJob
    let unownedJob = UnownedJob(job)
    let unownedExecutor = asUnownedSerialExecutor()
    // 在串行队列中执行任务
    serialQueue.async {
      unownedJob.runSynchronously(on: unownedExecutor)
    }
  }
 
  // 转换自身为 UnownedSerialExecutor
  public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
    UnownedSerialExecutor(ordinary: self)
  }
}

对于 Core Data 的应用场景,我们可以直接利用托管对象上下文的 perform 方法作为串行操作的工具。经过适当调整后的实现如下:

Swift
public final class NSModelObjectContextExecutor: @unchecked Sendable, SerialExecutor {
  public final let context: NSManagedObjectContext
  public init(context: NSManagedObjectContext) {
    self.context = context
  }

  public func enqueue(_ job: consuming ExecutorJob) {
    let unownedJob = UnownedJob(job)
    let unownedExecutor = asUnownedSerialExecutor()
    context.perform {
      unownedJob.runSynchronously(on: unownedExecutor)
    }
  }

  public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
    UnownedSerialExecutor(ordinary: self)
  }
}

通过将此执行者应用于 Actor,我们能够确保该 Actor 内的所有操作(构造函数除外)均在其托管对象上下文对应的线程上执行。

构建 Actor

Actor 中引入自定义执行者非常直接,仅需声明一个 unownedExecutor 属性。编译器识别到 Actor 包含此属性后,将通过此执行者进行任务调度。

Swift
public nonisolated var unownedExecutor: UnownedSerialExecutor

借此,我们便能够实现一个类似于 SwiftData,用于处理 Core Data 并发操作的 Actor

Swift
actor DataHandler {
  public nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
  public nonisolated let modelContainer: CoreData.NSPersistentContainer

  public init(container: CoreData.NSPersistentContainer) {
    // 初始化私有上下文
    let context = container.newBackgroundContext()
    // 实例化自定义执行者
    modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
    modelContainer = container
  }
  
  // 获取 Actor 所需的自定义执行者(UnownedSerialExecutor)
  public nonisolated var unownedExecutor: UnownedSerialExecutor {
    modelExecutor.asUnownedSerialExecutor()
  }

  // 用于 Actor 中数据操作的托管对象上下文
  public var modelContext: NSManagedObjectContext {
    modelExecutor.context
  }

  // 实现类似于 SwiftData 的托管对象访问机制
  public subscript<T>(id: NSManagedObjectID, as _: T.Type) -> T? where T: NSManagedObject {
    try? modelContext.existingObject(with: id) as? T
  }
}

实现 @NSModelActor 宏:简化 Core Data 并发操作

在 SwiftData 中,开发者仅需使用 @ModelActor 宏即可自动完成上文中的繁琐设置。为了提供相似的开发体验,我们引入了针对 Core Data 的 @NSModelActor 宏。

首先,我们对 Actor 的声明进行抽象化,引入 NSModelActor 协议:

Swift
public protocol NSModelActor: Actor {
  /// 为 NSModelActor 指定的 NSPersistentContainer
  nonisolated var modelContainer: NSPersistentContainer { get }

  /// 协调模型 actor 访问的执行者。
  nonisolated var modelExecutor: NSModelObjectContextExecutor { get }
}

extension NSModelActor {
  /// 模型 actor 的执行者的优化、非拥有引用。
  public nonisolated var unownedExecutor: UnownedSerialExecutor {
    modelExecutor.asUnownedSerialExecutor()
  }

  /// 序列化运行在模型 actor 上代码的上下文。
  public var modelContext: NSManagedObjectContext {
    modelExecutor.context
  }

  /// 根据指定的标识符返回模型,向下转型为适当的类。
  public subscript<T>(id: NSManagedObjectID, as _: T.Type) -> T? where T: NSManagedObject {
    try? modelContext.existingObject(with: id) as? T
  }
}

随后,声明 @NSModelActor 宏,该宏需符合 ExtensionMacroMemberMacro 协议:

Swift
@attached(member, names: named(modelExecutor), named(modelContainer), named(init))
@attached(extension, conformances: NSModelActor)
public macro NSModelActor() = #externalMacro(module: "CoreDataEvolutionMacrosPlugin", type: "NSModelActorMacro")

由于无需对原始代码进行任何特殊处理,因此宏的实现相对简单:

Swift
public enum NSModelActorMacro {}

extension NSModelActorMacro: ExtensionMacro {
  public static func expansion(of _: SwiftSyntax.AttributeSyntax, attachedTo _: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo _: [SwiftSyntax.TypeSyntax], in _: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
    // 生成符合 NSModelActor 协议的扩展代码
    let decl: DeclSyntax =
      """
      extension \(type.trimmed): CoreDataEvolution.NSModelActor {}
      """

    guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
      return []
    }

    return [extensionDecl]
  }
}

extension NSModelActorMacro: MemberMacro {
  public static func expansion(of _: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext) throws -> [DeclSyntax] {
    // 添加构造器及所需属性
    [
      """
      public nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
      public nonisolated let modelContainer: CoreData.NSPersistentContainer

      public init(container: CoreData.NSPersistentContainer) {
        let context = container.newBackgroundContext()
        modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
        modelContainer = container
      }
      """,
    ]
  }
}

现在,开发者就可以在 Core Data 中享受与 SwiftData 一样的优雅、安全的并发操作了!

SerialExecutorExecutorJob 仅支持在 iOS 17、macOS 14 及以上系统使用,目前还不能在较低版本的系统中应用。希望苹果公司能够像之前对并发模型所做的那样,将这部分 API 兼容到更低版本的系统,让更多开发者能够受益。

为便于广大开发者使用,我已将上述代码整合至 CoreDataEvolution 库中,并期待将 SwiftData 中获得的灵感逐步在此库中实现,同时也欢迎更多开发者的参与。

Let’s VisionOS 2024 活动中,我进行了题为 新框架、新思维:探索 Observation 与 SwiftData 的演讲,核心旨在于强调:虽然新框架旨在解决旧框架的问题,我们却不应受旧有经验和习惯的束缚。应以开放心态,从新角度学习和应用这些工具,把采纳新框架视为向更安全、更现代化转型的良机。

探索新框架的价值不仅在于应用它们的新 API,更重要的是,通过它们的设计理念启发我们优化传统框架的开发方式。因此,哪怕在长时间内你还不能使用 SwiftData、SwiftUI、Observation 等新框架,我依然鼓励开发者深入了解并学习它们,让这些新的设计理念丰富你的知识库。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK