尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Realm Swift SDK 与 Swift Actor 深度实战:Actor 隔离 Realm、跨 Actor 数据传递与异步通知

Realm Swift SDK 与 Swift Actor 深度实战:Actor 隔离 Realm、跨 Actor 数据传递与异步通知 数据库移动开发嵌入式数据库【免费下载链接】realm-swiftRealm is a mobile database: a replacement for Core Data SQLite项目地址https://gitcode.com/gh_mirrors/re/realm-swift点击查看免费下载导读本文基于 Realm Swift SDK10.39.0 起内置的 Swift Actor 支持系统讲解如何在并发场景下安全地使用 Realm从打开 Actor 隔离的 Realm、定义自定义 Realm Actor到跨 Actor 传递数据、执行异步写入与在指定 Actor 上观察变更。读完本文你将掌握一套用 Actor 取代手工线程/串行队列管理的完整异步数据库方案并能结合仓库源码理解其底层调度scheduler与写事务asyncWrite的实现原理。为什么要在 Actor 中使用 RealmRealm 的 live object 模型是**线程受限thread-confined**的Realm、对象与集合实例只在创建它们的线程上有效。传统做法是用 DispatchQueue 或手工线程切换来保证访问安全但 Swift 并发Swift 5.5 引入的 actor 模型提供了一种更结构化的替代方案。从 Realm Swift SDK 10.39.0 开始Realm 支持两种基于 Actor 的使用方式Actor 隔离的 Realmactor-isolated realm把全部 Realm 访问限制在单个特定 actor 上。好处是不需要在 actor 边界传递数据且能显著简化数据竞争data race的排查。跨 Actor 使用 Realm根据应用需要在不同 actor 上执行不同类型的任务。例如在MainActor上读取对象用后台 actor 执行大量写入。两种方式可以并存。对于 Swift actor 本身的通用知识建议先查阅 Apple 官方的 Swift 语言文档Actor 章节本文聚焦于 Realm 与 Actor 的结合用法。前置条件要在 Swift actor 中使用 Realm项目必须满足Realm Swift SDK 版本10.39.0 或更高Swift 5.8 / Xcode 14.3或更新版本。此外官方强烈建议在工程中开启以下两项设置对应 Realm.swift 初始化文档 中提到的推荐配置SWIFT_STRICT_CONCURRENCYcomplete开启严格的并发检查strict concurrency checking让编译器在编译期拦截跨隔离域的不安全访问OTHER_SWIFT_FLAGS-Xfrontend-enable-actor-data-race-checks开启运行时 actor 数据竞争检测。从源码看Realm 在首次使用时还会通过RLMSetMainActor(MainActor.shared)注册主 actor见 Realm.swift这是MainActor隔离 Realm 得以工作的基础。本文示例使用的数据模型下文所有示例共用以下Todo模型class Todo: Object { Persisted(primaryKey: true) var _id: ObjectId Persisted var name: String Persisted var owner: String Persisted var status: String }打开一个 Actor 隔离的 Realm使用 Swift 的async/await语法即可异步打开 Realm。直接使用try await Realm()打开的将是MainActor 隔离的 Realm通过try await Realm(actor:)显式指定隔离到的 actor。以下两个调用在语义上完全等价不传 actor 时async 初始化默认产出 MainActor 隔离的 RealmMainActor func mainThreadFunction() async throws { // These are identical: the async init produces a // MainActor-isolated Realm if no actor is supplied let realm1 try await Realm() let realm2 try await Realm(actor: MainActor.shared) try await useTheRealm(realm: realm1) }也可以在打开时指定默认配置或自定义配置例如按用户名生成不同的 Realm 文件MainActor func mainThreadFunction() async throws { let username Galadriel // Customize the default realm config var config Realm.Configuration.defaultConfiguration config.fileURL!.deleteLastPathComponent() config.fileURL!.appendPathComponent(username) config.fileURL!.appendPathExtension(realm) // Open an actor-isolated realm with a specific configuration let realm try await Realm(configuration: config, actor: MainActor.shared) try await useTheRealm(realm: realm) }关于 Realm 配置的通用说明文件路径、加密、迁移等参见 docs/guides/realm-files/configure-and-open-a-realm.md。源码视角async 初始化做了什么在 Realm.swift 中带 actor 的初始化实现为public initA: Actor(configuration: Realm.Configuration .defaultConfiguration, actor: A) async throws { let scheduler RLMScheduler.actor(actor, invoke: actor.invoke, verify: await actor.verifier()) let rlmRealm try await openRealm(configuration: configuration, scheduler: scheduler, actor: actor) self Realm(rlmRealm.wrappedValue) }可以看到核心机制是构造一个基于该 actor 的RLMScheduler把 Realm 的一切调度工作绑定到 actor 的 executor 上而openRealm会把创建、迁移、压缩文件以及同步 Realm 的数据下载等初始化工作放到后台线程执行不阻塞调用方的 executor见 Realm.swift 的注释。在 Swift 6 编译器下还提供了Realm.open(configuration:_isolation:)静态方法Realm.swift可利用#isolation自动推断当前 actor 隔离域。定义自定义 Realm Actor你可以定义一个专门的 actor 来管理 Realm 访问与写操作。注意在init中把self传给Realm(actor:)时需要用隐式解包可选implicitly-unwrapped optional来存储 realmactor RealmActor { // An implicitly-unwrapped optional is used here to let us pass self to // Realm(actor:) within init var realm: Realm! init() async throws { realm try await Realm(actor: self) } var count: Int { realm.objects(Todo.self).count } func createTodo(name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ _id: ObjectId.generate(), name: name, owner: owner, status: status ]) } } func getTodoOwner(forTodoNamed name: String) - String { let todo realm.objects(Todo.self).where { $0.name name }.first! return todo.owner } struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) - TodoStruct { let todo realm.objects(Todo.self).where { $0.name name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) } func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ _id: _id, name: name, owner: owner, status: status ], update: .modified) } } func deleteTodo(id: ObjectId) async throws { try await realm.asyncWrite { let todoToDelete realm.object(ofType: Todo.self, forPrimaryKey: id) realm.delete(todoToDelete!) } } func close() { realm nil } }Actor 隔离的 Realm 既可以配合局部 actorlocal actor也可以配合全局 actorglobal actor使用// A simple example of a custom global actor globalActor actor BackgroundActor: GlobalActor { static var shared BackgroundActor() } BackgroundActor func backgroundThreadFunction() async throws { // Explicitly specifying the actor is required for anything that is not MainActor let realm try await Realm(actor: BackgroundActor.shared) try await realm.asyncWrite { _ realm.create(Todo.self, value: [ name: Pledge fealty and service to Gondor, owner: Pippin, status: In Progress ]) } // Thread-confined Realms would sometimes throw an exception here, as we // may end up on a different thread after an await let todoCount realm.objects(Todo.self).count print(The number of Realm objects is: \(todoCount)) } MainActor func mainThreadFunction() async throws { try await backgroundThreadFunction() }注意对于MainActor之外的任何 actor都必须显式传入Realm(actor:)。上面示例特意在await之后继续访问realm.objects(...).count以说明线程受限的 Realm 在await之后可能落在不同线程上从而抛异常而 actor 隔离的 Realm 则不会。在隔离函数中同步使用 Realm Actor当某个函数被隔离到特定 actor时可以直接同步使用该 actor 隔离的 Realm无需 async/await 关键字func createObject(in actor: isolated RealmActor) async throws { // Because this function is isolated to this actor, you can use // realm synchronously in this context without async/await keywords try actor.realm.write { actor.realm.create(Todo.self, value: [ name: Keep it secret, owner: Frodo, status: In Progress ]) } let taskCount actor.count print(The actor currently has \(taskCount) tasks) } let actor try await RealmActor() try await createObject(in: actor)在异步函数中使用 Realm Actor当函数不隔离到该 actor 时就必须借助 async/await 语法等待 actor 上的操作完成func createObject() async throws { // Because this function is not isolated to this actor, // you must await operations completed on the actor try await actor.createTodo(name: Take the ring to Mount Doom, owner: Frodo, status: In Progress) let taskCount await actor.count print(The actor currently has \(taskCount) tasks) } let actor try await RealmActor() try await createObject()注意这里actor变量在示例中来自外部上下文即前面let actor try await RealmActor()创建的实例。两种写法同步隔离 vs 异步等待的选择标准很简单——你的函数是否被隔离到该 actor。向 Actor 隔离的 Realm 写入数据Actor 隔离的 Realm 支持用 async/await 语法进行异步写入。try await realm.asyncWrite { ... }会挂起suspend当前任务在不阻塞当前线程的前提下获取写锁调用写入 block由后台线程把数据写盘完成后恢复任务。用上面RealmActor中的createTodo即可演示func createTodo(name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ _id: ObjectId.generate(), name: name, owner: owner, status: status ]) } }在非隔离函数中通过 async 语法触发写入func createObject() async throws { // Because this function is not isolated to this actor, // you must await operations completed on the actor try await actor.createTodo(name: Take the ring to Mount Doom, owner: Frodo, status: In Progress) let taskCount await actor.count print(The actor currently has \(taskCount) tasks) } let actor try await RealmActor() try await createObject()与writeAsync()基于 completion handler、写 block 在调用线程执行不同asyncWrite()在等待写锁时挂起任务而不是阻塞线程并且写盘 I/O 由后台工作线程完成。因此对于小写入即使在MainActor函数中使用也不会阻塞 UI。不过由于复杂度和平台资源限制而对性能有显著影响的写入仍然建议放到后台线程执行。限制异步写入仅支持 actor 隔离的 Realm或在MainActor函数中使用。源码视角asyncWrite 的实现在 Realm.swift 中asyncWrite首先检查rlmRealm.actor若不是 actor 隔离的 Realm 会直接fatalError(asyncWrite() can only be called on main thread or actor-isolated Realms)。随后调用realm.beginAsyncWrite()开启异步写事务并用withTaskCancellationHandler等待写锁任务被取消时通过actor.invoke在 actor 上完成complete写事务以释放锁。block 抛出错误时若仍处于写事务中则调用cancelWriteTransaction()回滚最后通过commitAsyncWrite(withGrouping: false)提交。在开始写事务前asyncWrite还会把 Realm 更新到最新版本相当于调用了一次asyncRefresh()并在此过程中派发通知见 Realm.swift 的注释。仓库测试 RealmTests.swift 中的testAsyncRefresh展示了跨 actor 写入后通过asyncRefresh()看到最新数据的完整流程。跨 Actor 边界传递 Realm 数据Realm 对象不是 Sendable不能直接跨越 actor 边界。官方提供两种方案传递ThreadSafeReference传递其他 Sendable 类型如直接传值或构造 struct 表示。方案一传递 ThreadSafeReference在拥有对象的 actor如下例的MainActor上创建ThreadSafeReference再传给目标 actor// We can pass a thread-safe reference to an object to update it on a different actor. let todo todoCollection.where { $0.name Arrive safely in Bree }.first! let threadSafeReferenceToTodo ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)在目标 actor 上必须在写事务内调用resolve()才能使用它——resolve会取回该 actor 本地版本的对象actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReferenceTodo) throws { let realm try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor realm.resolve(tsr) realm.delete(todoOnActor!) } } }重要ThreadSafeReference必须恰好被 resolve 一次。否则源 Realm 会一直处于 pinned 状态直到引用被释放。因此ThreadSafeReference应当是短命的。如果需要在多个 actor 间多次共享同一个 Realm 对象更推荐共享主键再在目标 actor 上查询。参见下文「传递主键并在另一 Actor 上查询」。从源码看ThreadSafeReference结构体本身声明为SendableThreadSafeReference.swift 定义结构体第 219 行extension ThreadSafeReference: Sendable这正是它能跨 actor 传递的类型基础其文档同时明确警告must be resolved at most onceThreadSafeReference.swift。方案二传递 Sendable 类型由于 Realm 对象不是 Sendable可以改用 Sendable 数据来跨 actor 工作常见三种策略1. 传递 Sendable 的 Realm 类型与原始值如果只需要对象的某一条信息如String、Int直接传值即可而不是传递整个 Realm 对象。哪些 Realm 类型是 Sendable 的完整清单参见 docs/guides/swift-concurrency.md 中的「Sendable, Non-Sendable and Thread-Confined Types」一节。MainActor func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. // Pass primitive data to the actor instead of // creating the object here and passing the object. let actor try await RealmActor() try await actor.createTodo(name: Prepare fireworks for birthday party, owner: Gandalf, status: In Progress) // Later, get information off the actor-confined realm let todoOwner await actor.getTodoOwner(forTodoNamed: Prepare fireworks for birthday party) }2. 传递主键并在另一 Actor 上查询如果想在另一个 actor 上使用 Realm 对象可以共享对象的主键然后在目标 actor 上查询// Execute code on a specific actor - in this case, the MainActor MainActor func mainThreadFunction() async throws { // Create an object off the main actor func createObject(in actor: isolated BackgroundActor) async throws - ObjectId { let realm try await Realm(actor: actor) let newTodo try await realm.asyncWrite { return realm.create(Todo.self, value: [ name: Pledge fealty and service to Gondor, owner: Pippin, status: In Progress ]) } // Share the todos primary key so we can easily query for it on another actor return newTodo._id } // Initialize an actor where you want to perform background work let actor BackgroundActor() let newTodoId try await createObject(in: actor) let realm try await Realm() let todoOnMainActor realm.object(ofType: Todo.self, forPrimaryKey: newTodoId) }这里asyncWrite的 block 直接返回realm.create(...)的结果说明asyncWrite支持带返回值的闭包——返回值会作为asyncWrite的结果返回给调用方。3. 创建对象的 Sendable 表示struct如果需要传递的不止一个简单值又不愿背负ThreadSafeReference的开销或在各 actor 上重复查询可以创建 struct 等 Sendable 表示struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) - TodoStruct { let todo realm.objects(Todo.self).where { $0.name name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) }然后在另一个 actor 上调用该函数获取 structMainActor func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. let actor try await RealmActor() try await actor.createTodo(name: Leave the ring on the mantle, owner: Bilbo, status: In Progress) // Get information as a struct or other Sendable type. let todoAsStruct await actor.getTodoAsStruct(forTodoNamed: Leave the ring on the mantle) }在不同 Actor 上观察通知可以使用 async/await 语法在 actor 隔离的 Realm 上观察通知调用await object.observe(on: Actor)或await collection.observe(on: Actor)即可注册一个在对象或集合每次变化时被调用的 block。SDK 会在指定 actor 的 executor 上异步调用该 block对于在不同线程或不同进程中执行的写事务SDK 会在 Realm自动刷新到包含这些变更的版本后调用 block对于本地写入SDK 会在写事务提交后的某个时刻调用 block与普通 Realm 通知一样只能观察由 Realm 管理的对象或集合且必须持有返回的 token直到不再需要观察为止如果需要手动推进主线程或其他 actor 上被观察 Realm 的状态调用await realm.asyncRefresh()。这会更新 Realm 及其管理的未完成对象到最新数据并派发适用的通知其实现见 Realm.swift同样限定在主线程或 actor 隔离的 Realm。观察的限制不能在以下情况调用.observe()写事务进行中during a write transaction所在 Realm 为只读read-only时在 actor 隔离的 Realm 上、从该 actor外部调用即必须从 actor 内部观察其隔离的 Realm。注册集合变更监听在每次写事务之后若出现以下情况SDK 会调用集合通知 block从集合中删除对象向集合中插入对象修改集合中对象的任意受管属性包括把属性设置为其当前值的自我赋值。重要在集合通知处理器中必须按 删除 → 插入 → 修改 的顺序应用变更。先处理插入再处理删除可能导致意外行为。这些通知还会提供变更发生的 actor信息。与普通集合通知相同它们也提供change参数RealmCollectionChange报告写事务中被删除、新增或修改的对象并解析为索引路径数组可直接传给UITableView的批量更新方法。// Create a simple actor actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReferenceTodo) throws { let realm try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor realm.resolve(tsr) realm.delete(todoOnActor!) } } } // Execute some code on a different actor - in this case, the MainActor MainActor func mainThreadFunction() async throws { let backgroundActor BackgroundActor() let realm try! await Realm() // Create a todo item so there is something to observe try await realm.asyncWrite { realm.create(Todo.self, value: [ _id: ObjectId.generate(), name: Arrive safely in Bree, owner: Merry, status: In Progress ]) } // Get the collection of todos on the current actor let todoCollection realm.objects(Todo.self) // Register a notification token, providing the actor where you want to observe changes. // This is only required if you want to observe on a different actor. let token await todoCollection.observe(on: backgroundActor, { actor, changes in print(A change occurred on actor: \(actor)) switch changes { case .initial: print(The initial value of the changed object was: \(changes)) case .update(_, let deletions, let insertions, let modifications): if !deletions.isEmpty { print(An object was deleted: \(changes)) } else if !insertions.isEmpty { print(An object was inserted: \(changes)) } else if !modifications.isEmpty { print(An object was modified: \(changes)) } case .error(let error): print(An error occurred: \(error.localizedDescription)) } }) // Update an object to trigger the notification. // This example triggers a notification that the object is deleted. // We can pass a thread-safe reference to an object to update it on a different actor. let todo todoCollection.where { $0.name Arrive safely in Bree }.first! let threadSafeReferenceToTodo ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing token.invalidate() }集合通知的observeA: Actor(keyPaths:on:_:)泛型签名定义在 RealmCollection.swift 中block 类型为Sendable escaping (isolated A, RealmCollectionChangeSelf) - Void——block 第一个参数就是被隔离的 actor保证回调在指定 actor 上执行。注册对象变更监听SDK 会在每次写事务后调用对象通知 block条件是对象被删除对象的任意受管属性被修改包括把属性设置为其当前值的自我赋值。block 会收到隔离到请求 actor 的对象副本以及变更信息。这个对象可以安全地在该 actor 上使用。默认情况下只有对对象属性的直接修改才会产生通知对链接对象的修改不会产生通知。如果传入非 nil、非空的 keypath 数组则只有这些 keypath 标识的属性变化才会触发通知keypath 可以穿越链接属性link properties以接收链接对象的变化。// Execute some code on a specific actor - in this case, the MainActor MainActor func mainThreadFunction() async throws { // Initialize an instance of another actor // where you want to do background work let backgroundActor BackgroundActor() // Create a todo item so there is something to observe let realm try! await Realm() let scourTheShire try await realm.asyncWrite { return realm.create(Todo.self, value: [ _id: ObjectId.generate(), name: Scour the Shire, owner: Merry, status: In Progress ]) } // Register a notification token, providing the actor let token await scourTheShire.observe(on: backgroundActor, { actor, change in print(A change occurred on actor: \(actor)) switch change { case .change(let object, let properties): for property in properties { print(Property \(property.name) of object \(object) changed to \(property.newValue!)) } case .error(let error): print(An error occurred: \(error)) case .deleted: print(The object was deleted.) } }) // Update the object to trigger the notification. // This triggers a notification that the objects status property has been changed. try await realm.asyncWrite { scourTheShire.status Complete } // Invalidate the token when done observing token.invalidate() }常见并发陷阱与排查建议结合 docs/guides/swift-concurrency.md 中的并发注意事项使用 actor 时仍需留意await是潜在的挂起点Swift 5.7 之后挂起后恢复的代码可能运行在不同线程上。因此在线程受限的 Realm 上await之后继续访问 Realm 可能报Realm accessed from incorrect thread.这正是上文中backgroundThreadFunction示例想要避免的情况。解决之道是使用 actor 隔离的 Realm或在访问 Realm 的函数上标注MainActor。Task / TaskGroup 同理任务中包含await时后续代码可能在不同线程恢复。如果任务里要访问 Realm需标注MainActor使用 actor 隔离的 Realm 后可以直接把任务隔离到对应 actor 上。异步写入的适用范围asyncWrite/asyncRefresh只能用于 actor 隔离的 Realm 或MainActor函数见 Realm.swift 的运行时检查对非 actor 隔离的 Realm传统方案是writeAsync()completion handler 风格。类型分类Realm Swift SDK 公开 API 中的类型分为三类——Sendable、非 Sendable 且非线程受限、线程受限。线程受限类型除非 frozen被限制在某个隔离上下文内即使加同步也不能跨上下文传递非 Sendable 且非线程受限的类型可以共享但必须自行同步。详见 docs/guides/swift-concurrency.md 的「Sendable, Non-Sendable and Thread-Confined Types」一节。小结Realm Swift SDK 10.39.0 的 Actor 支持为移动端数据库并发访问提供了一条结构化路径用try await Realm()或try await Realm(actor:)打开 actor 隔离的 Realm自定义RealmActor封装全部 Realm 访问写入统一走asyncWrite挂起而非阻塞、后台线程落盘跨 actor 传递数据时依据场景选择ThreadSafeReference短命、恰好 resolve 一次、Sendable 原始值、主键查询或 struct 表示用await collection.observe(on:)/await object.observe(on:)把变更通知投递到指定 actor必要时用asyncRefresh()手动推进版本。配合工程中的SWIFT_STRICT_CONCURRENCYcomplete与运行时 actor 数据竞争检测即可在编译期和运行期双重保障并发安全。若需深入了解线程受限模型的边界与历史演进可继续阅读仓库中的 docs/guides/swift-concurrency.md并对照 Realm.swift、ThreadSafeReference.swift 与 RealmTests.swift 中的测试用例加深理解。赞分享数据库移动开发嵌入式数据库【免费下载链接】realm-swiftRealm is a mobile database: a replacement for Core Data SQLite项目地址https://gitcode.com/gh_mirrors/re/realm-swift点击查看免费下载相关推荐Realm Swift SDK 与 Swift 并发await 线程跳转、异步写入与 Actor 隔离实战指南Realm Swift SDK 与 Swift 并发await 线程跳转、异步写入与 Actor 隔离实战指南 导读 本文基于 Realm Swift SDK数据库移动开发嵌入式数据库Realm Swift SDK 在 SwiftUI 视图间传递 Realm 数据对象传递与环境值注入完整指南Realm Swift SDK 在 SwiftUI 视图间传递 Realm 数据对象传递与环境值注入完整指南 在 SwiftUI 应用中如何优雅地在视图层级数据库移动开发嵌入式数据库Ray Actor 并发指南AsyncIO 异步 Actor、线程 Actor 与 max_concurrency 实践Ray Actor 并发指南AsyncIO 异步 Actor、线程 Actor 与 max_concurrency 实践 导读 在 Ray 中单个 Acto人工智能分布式训练强化学习任务调度模型推理服务上一篇OBS Studio浏览器插件异常高CPU占用问题分析与解决方案下一篇SDLPAL深度解析跨平台游戏复刻引擎的技术架构与实战应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表