You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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)
}

问题根源与解决方法

核心问题:线程安全冲突

  1. SwiftData模型对象不能跨线程操作:你在后台线程(Task.detached)里创建了Item实例,虽然AppState是@Observable,修改属性会触发主线程更新,但Item作为SwiftData模型,其内部状态依赖模型上下文,跨线程操作未插入的模型对象会引发线程竞争。
  2. @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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 13:04:55