Swift Task内修改可变Struct是否安全?是否需使用Actor?
核心问题:共享可变状态的线程不安全
你代码里的items和uniqueKeys是MyModelRepository这个类的属性,虽然它们是值类型(Array、Set),但因为被类持有,多个线程同时修改的本质是共享可变状态——主线程的removeAll()和Task后台线程的items.removeAll { ... }/uniqueKeys.remove(...)在并发执行,这会直接引发问题:
- 比如主线程清空数组的同时,后台线程在遍历删除元素,数组内部结构被并发修改,直接触发崩溃;
- 即使没崩溃,
uniqueKeys和items的状态也会不一致,传给Diffable Data Source的快照会出现数据不匹配,导致UI错乱或后续崩溃。
值类型默认Sendable不代表多线程修改安全,Sendable只是保证类型可以安全跨线程传递,但修改同一个类持有的值类型实例依然会有线程安全问题——因为Array/Set的remove等mutating方法是原地修改自身,并发操作会破坏内部结构。
要不要用Actor?必须用(或等价的线程同步方案)
Actor是Swift解决共享可变状态线程安全的最优方案,它会自动确保所有内部状态的访问都是串行的,不会出现并发冲突。你可以直接把MyModelRepository改成Actor:
actor MyModelRepository { private var items: [MyModel] = [] private(set) var uniqueKeys = Set<String>() func removeAll() { items.removeAll() uniqueKeys.removeAll() } func eventHandler() { do { let updates = try await retrieveUpdates() // 发送Publisher要切回主线程,因为UI侧订阅在主线程 DispatchQueue.main.async { self.updateItemsPublisher.send(updates.updatedValues) } for item in updates.removed { items.removeAll { $0 == item } uniqueKeys.remove(item.key.orEmpty) guard let deletedModel = // 本地查找逻辑 else { continue } DispatchQueue.main.async { self.deleteItemPusblisher.send(deletedModel) } } } catch { // 错误日志 } } }
这样所有对items和uniqueKeys的修改都会在Actor的串行执行器中完成,彻底避免并发冲突。
替代方案:强制所有修改在主线程执行
如果不想用Actor,也可以把所有状态修改逻辑都切到主线程,比如把Task里的修改代码放到DispatchQueue.main.async中:
func eventHandler() { Task { do { let updates = try await retrieveUpdates() DispatchQueue.main.async { self.updateItemsPublisher.send(updates.updatedValues) for item in updates.removed { self.items.removeAll { $0 == item } self.uniqueKeys.remove(item.key.orEmpty) guard let deletedModel = // 本地查找逻辑 else { continue } self.deleteItemPusblisher.send(deletedModel) } } } catch { // 错误日志 } } }
这种方案更简单,但Actor的优势是编译时就检查线程安全,不会漏掉某个修改逻辑,适合复杂的状态管理场景。
崩溃根源
Diffable Data Source依赖数据源的一致性——生成快照时会假设数据源是稳定的。如果后台线程在修改数据源的同时,主线程正在生成快照,就会拿到半修改的状态,导致Diffable Data Source内部断言失败或数据不匹配,最终引发崩溃。
内容的提问来源于stack exchange,提问作者Lorenzo B

