我很好奇是否有人能轻易发现我的错误。我遇到了一个问题,我的应用程序在调用 sleep 时挂起。我最好的猜测是,这是使用 SwiftData 和 @Observable 导致的死锁。我已将代码精简为一个最小的 SwiftUI 应用程序,该应用程序在按下“开始”按钮几秒钟后始终挂起(Xcode 15.4)。我可能做错了什么,但我无法发现它。
代码如下:
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(用将保存在本地状态存储中的普通结构替换 Item),那么就不会出现问题。此外,如果我将队列数组从 AppState @Observable 移动到本地状态存储,那么就不会出现问题。因此,我有些不确定地得出结论,问题与两者的结合有关。有人能指出我做错了什么吗?
Task.detached
采用@Sendable
闭包,它不能捕获非Sendable
事物。但是,您使用的闭包捕获[Item]
、ContentView
和AppState
,它们都是非Sendable
。如果您打开完整的并发检查,您的代码中将出现许多警告。您应该将整体隔离
ContentView
为,MainActor
而不是将其各个方法标记为@MainActor
。然后,使用
.task(id:)
修饰符而不是 来启动任务Task.detached
。如果要在非主线程上运行某些操作,请将其放在异步nonisolated func
或与另一个参与者隔离的函数中。SwiftData 模型不是- 因此“一个创建并将它们添加到队列,另一个将它们从队列中取出”
Sendable
的想法行不通。您应该使用某种东西,例如具有所有属性的简单结构,其中包含创建 所需的一切。您应该在将其放入上下文之前创建。Task.detached
Item
Task.detached
Sendable
let
Item
Item
insert
经过这些转换之后的代码如下:
AppState
一个代表项目名称的字符串数组。这是Sendable
,因此可以将其发送给模型参与者进行插入fetchItemName
是非隔离的,因此当你使用await
它时它不会在主线程上运行。insertItems(withNames:)
与 隔离ItemsActor
,因此它也不会在主线程上运行。syncQueuedItems
与您的代码略有不同。我只是将队列中的所有内容取出并插入所有内容。也可以按照您的方式进行操作,但这将涉及主要参与者和之间的大量跳跃ItemsActor
。