Swift中如何以异步友好方式保护resetLobby临界区避免竞态条件?
解决Swift异步场景下的大厅监听器竞态问题
问题代码
func resetLobby() async { do { lobbyViewModel.pauseListener() let question = lobbyViewModel.currentLobby?.questionDeck?.currentQuestion if let winner, let question { print("Reset Lobby: ") try await lobbyViewModel.resetLobby(winner: winner, question: question) } try await lobbyViewModel.nextQuestion() lobbyViewModel.restartListener() // try await lobbyViewModel.nextQuestion() if let id = userViewModel.currentUser?.id, let num = lobbyViewModel.currentLobby?.number { try await userViewModel.loadUser(userId: id, lobbyNumber: num) } } catch { award = true } }
问题描述
开发多用户大厅应用时,每个用户绑定了大厅状态更新的监听器。当前resetLobby()执行期间,大厅级监听器会被触发,导致返回给用户的属性信息错误——现有暂停/启动监听器仅对本地生效,无法控制全局监听器。需要一种Swift异步友好的方案,将resetLobby()标记为临界区,确保监听器在其执行完成后才触发。
解决方案
1. 用Swift Actor实现状态隔离(推荐)
Swift的Actor是官方原生的异步安全机制,能自动保证所有异步方法串行执行,从根源上避免竞态条件。将大厅核心逻辑封装到Actor中,所有状态修改和监听器触发都在Actor内部完成:
actor LobbyViewModel { private var currentLobby: Lobby? private var questionDeck: QuestionDeck? private var listeners: [LobbyStateListener] = [] // 重置大厅核心逻辑 func resetLobby(winner: Winner, question: Question) async throws { // 原重置逻辑实现 } // 切换下一题逻辑 func nextQuestion() async throws { // 原下一题逻辑实现 } // 添加监听器 func addListener(_ listener: LobbyStateListener) { listeners.append(listener) } // 仅在状态更新完成后通知监听器 private func notifyListeners() async { guard let lobby = currentLobby else { return } for listener in listeners { await listener.onLobbyStateUpdated(lobby) } } // 原子化的重置流程 func runResetFlow(winner: Winner?) async { do { let currentQuestion = currentLobby?.questionDeck?.currentQuestion if let winner, let question = currentQuestion { print("Reset Lobby: ") try await resetLobby(winner: winner, question: question) } try await nextQuestion() // 加载用户信息(如果依赖大厅状态,需在Actor内执行或保证串行) if let userId = UserViewModel.shared.currentUser?.id, let lobbyNum = currentLobby?.number { try await UserViewModel.shared.loadUser(userId: userId, lobbyNumber: lobbyNum) } // 所有操作完成后,统一触发监听器 await notifyListeners() } catch { // 错误处理逻辑 } } }
说明:Actor会自动对内部状态的访问加锁,runResetFlow作为一个完整的原子操作执行,只有当所有重置步骤完成后,才会调用notifyListeners触发状态更新,彻底避免执行过程中监听器被触发的问题。
2. 异步锁实现临界区(轻量替代)
如果无法将整个ViewModel重构为Actor,可以实现一个简单的AsyncLock,将resetLobby()的逻辑包裹在临界区内:
class AsyncLock { private let semaphore = DispatchSemaphore(value: 1) func execute<T>(_ block: () async throws -> T) async rethrows -> T { semaphore.wait() defer { semaphore.signal() } return try await block() } }
在原ViewModel中使用该锁:
class LobbyViewModel { private let asyncLock = AsyncLock() // 其他属性... func resetLobby() async { try? await asyncLock.execute { [weak self] in guard let self = self else { return } // 原resetLobby的所有逻辑 let question = self.currentLobby?.questionDeck?.currentQuestion if let winner, let question { print("Reset Lobby: ") try await self.resetLobby(winner: winner, question: question) } try await self.nextQuestion() if let id = self.userViewModel.currentUser?.id, let num = self.currentLobby?.number { try await self.userViewModel.loadUser(userId: id, lobbyNumber: num) } } // 所有临界区操作完成后,手动触发监听器(或确保监听器仅读取最终状态) self.notifyListeners() } private func notifyListeners() { // 触发监听器逻辑 } }
说明:AsyncLock通过信号量保证临界区内的代码串行执行,任何尝试访问大厅状态的操作(包括监听器的状态读取)都会等待锁释放,确保监听器看到的是重置完成后的最终状态。
内容的提问来源于stack exchange,提问作者lionsfan
相关产品推荐
相关产品推荐

