使用SwiftData与@Observable时如何避免死锁/应用挂起?
SwiftData与@Observable组合导致应用挂起问题分析
我怀疑自己犯了个低级错误,但一直没找到。我的应用在调用sleep时会出现挂起问题,推测是SwiftData和@Observable组合使用导致的死锁。我把代码简化成了一个最小化的SwiftUI应用(Xcode 15.4),点击“Begin”按钮几秒后就会持续挂起。
我发现几个关键现象:
- 不使用SwiftData(把Item换成普通结构体存在本地State里),问题就消失了
- 把队列数组从@Observable的AppState移到本地State存储,问题也会消失
因此我推断问题和这两者的组合有关,恳请有人指出我哪里错了?
简化后的测试代码
import SwiftUI import SwiftData import os private let logger = Logger(subsystem: "TestApp", category: "General") @Observable class AppState { var queue: [Item] = [] } @Model final class Item { var name: String init(name: String) { self.name = name } } struct ContentView: View { @Environment(\.modelContext) private var modelContext @Query private var items: [Item] @State private var state = AppState() // @State private var queue = [Item]() @State private var testsRunning = false @State private var remoteTask: Task<(), Never>? @State private var syncTask: Task<(), Never>? var body: some View { VStack { Button ("Begin") { Task { await runTests() } testsRunning = true }.disabled(testsRunning) Text("Remote Queue: \(state.queue.count)") List (items) { Text($0.name) } } } } extension ContentView { @MainActor func runTests() async { for item in items { modelContext.delete(item) } state.queue.removeAll() startRemoteWork() startSync() } @MainActor func startRemoteWork() { // Adds non-inserted SwiftData items in an array to simulate data in cloud remoteTask = Task.detached { while true { await sleep(duration: .random(in: 0.2...0.5)) let newItem = Item(name: "Item \(items.count + state.queue.count + 1)") state.queue.append(newItem) logger.info("\(Date.now): \(newItem.name) added to remote queue") } } } @MainActor func syncQueuedItems() async { // removes items from remote queue and inserts them into local SwiftData context. while !state.queue.isEmpty { let item = state.queue.removeFirst() modelContext.insert(item) let delay = Double.random(in: 0.01...0.05) logger.info(" \(Date.now): syncing \(item.name) (will take \(delay) seconds)...") await sleep(duration: delay) // simulating work logger.info(" \(Date.now): Done") } } @MainActor func startSync() { syncTask = Task.detached { logger.info(" \(Date.now): Sync Task Started") while true { await syncQueuedItems() logger.info(" \(Date.now): Sync Task sleeping for 3 seconds till next sync") await sleep(duration: 3) } } } func sleep(duration: Double) async { do { try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) } catch { fatalError("Sleep failed") } } } #Preview { ContentView() .modelContainer(for: Item.self, inMemory: true) }
问题根源与解决方法
核心问题:线程安全冲突
- SwiftData模型对象不能跨线程操作:你在后台线程(
Task.detached)里创建了Item实例,虽然AppState是@Observable,修改属性会触发主线程更新,但Item作为SwiftData模型,其内部状态依赖模型上下文,跨线程操作未插入的模型对象会引发线程竞争。 - @Observable的主线程绑定加剧冲突:后台任务修改
state.queue时,@Observable会隐式在主线程更新UI,同时syncQueuedItems持续对state.queue进行读写,多线程资源竞争最终导致死锁。
修复方案
方案一:确保SwiftData对象操作在主线程
修改startRemoteWork,将创建Item和更新队列的逻辑切回主线程:
@MainActor func startRemoteWork() { remoteTask = Task.detached { while true { await sleep(duration: .random(in: 0.2...0.5)) // 切回主线程处理SwiftData对象与队列更新 await MainActor.run { let newItem = Item(name: "Item \(self.items.count + self.state.queue.count + 1)") self.state.queue.append(newItem) logger.info("\(Date.now): \(newItem.name) added to remote queue") } } } }
方案二:用普通结构体暂存远程数据
彻底避免跨线程操作SwiftData对象,先把远程数据存在普通结构体中,同步时再转为模型实例:
// 新增普通结构体存储远程数据 struct RemoteItem { let name: String } @Observable class AppState { var queue: [RemoteItem] = [] // 替换为普通结构体数组 } // 修改startRemoteWork @MainActor func startRemoteWork() { remoteTask = Task.detached { while true { await sleep(duration: .random(in: 0.2...0.5)) await MainActor.run { let newRemoteItem = RemoteItem(name: "Item \(self.items.count + self.state.queue.count + 1)") self.state.queue.append(newRemoteItem) logger.info("\(Date.now): \(newRemoteItem.name) added to remote queue") } } } } // 修改syncQueuedItems @MainActor func syncQueuedItems() async { while !state.queue.isEmpty { let remoteItem = state.queue.removeFirst() let item = Item(name: remoteItem.name) // 转为SwiftData模型 modelContext.insert(item) let delay = Double.random(in: 0.01...0.05) logger.info(" \(Date.now): syncing \(item.name) (will take \(delay) seconds)...") await sleep(duration: delay) logger.info(" \(Date.now): Done") } }
额外优化:管理后台任务生命周期
在视图消失时取消后台任务,避免内存泄漏:
struct ContentView: View { // ... 原有代码 ... var body: some View { VStack { // ... 原有视图代码 ... } .onDisappear { remoteTask?.cancel() syncTask?.cancel() } } }
内容的提问来源于stack exchange,提问作者Curious Jorge
相关产品推荐
相关产品推荐

