多协程操作受Mutex保护的Map时,协程取消的正确处理方案
嘿,这个问题确实是协程+Mutex组合使用时的典型痛点,我来给你梳理下可行的解决方案,结合你的代码场景来说明:
首先明确:协程取消时,Mutex会自动释放锁
你不用太担心协程被取消时锁还被持有——Mutex.withLock()是挂起函数,它内部用了try-finally块,不管协程是正常结束还是被取消抛出CancellationException,finally块都会执行锁的释放操作。所以只要你是通过withLock()来获取锁的,就不会出现锁永久占用的情况。
实践方案:跟踪协程Job,优雅控制取消与清理
1. 给每个需要控制的协程保存Job实例
在你的Fragment里,给每个观察者启动的协程单独维护一个Job?变量,比如:
private var flaggedMapJob: Job? = null private var filteredMapJob: Job? = null
这样在需要取消旧任务时,直接调用flaggedMapJob?.cancel()就能终止对应的协程。
2. 取消旧任务后,等待锁释放再启动新任务(可选但推荐)
如果你的新任务需要立即操作Map,最好等待旧协程完全结束(确保锁已经释放),可以用join()来实现,但注意不要在主线程直接阻塞,而是在协程里调用:
// 在观察者收到新数据时 flaggedMapJob?.cancel() lifecycleScope.launchWhenStarted { flaggedMapJob?.join() // 等待旧协程结束,锁释放 // 启动新的协程任务 flaggedMapJob = launch { // 你的Map操作逻辑 } }
3. 清理操作必须在Mutex保护下执行
不管是在旧协程的finally块里清理,还是单独启动协程清理,只要涉及修改GoogleMap(比如移除标记、移动相机),都要放在mapMutex.withLock()里面,避免和其他协程的Map操作冲突。比如你无数据时的清理逻辑:
lifecycleScope.launchWhenStarted { mapMutex.withLock { withContext(Dispatchers.Main) { selectedSiteMarker?.remove() selectedCircle?.remove() } } }
4. 批量/全部取消的技巧
如果需要一次性取消多个协程,可以把它们绑定到同一个父Job上:
private val parentJob = Job() // 启动协程时指定父Job flaggedMapJob = lifecycleScope.launch(parentJob) { ... } filteredMapJob = lifecycleScope.launch(parentJob) { ... } // 取消所有子协程 parentJob.cancel()
这样调用parentJob.cancel()就能一次性取消所有关联的协程,每个协程的锁都会自动释放。
针对你代码的优化建议
结合你的代码片段,修改后的核心逻辑大概是这样:
class HomeFragment:Fragment(){ ... private lateinit var googleMap:GoogleMap val mapMutex = Mutex() private var flaggedMapJob: Job? = null private var filteredMapJob: Job? = null ... override fun onViewCreated(view:View, savedInstanceState: Bundle?) { ... binding.fragmentHomeMapView?.getMapAsync { _googleMap -> _googleMap?.let{ googleMap = it } ?: run { Message.error("Error creating map (null)") } } ... homeViewModel.apply { liveDataMapFlagged?.observe(viewLifeCycleOwner) { flaggedMapDetailResult -> // 先取消旧任务 flaggedMapJob?.cancel() flaggedMapDetailResult?.apply { flaggedMapJob = lifecycleScope.launchWhenStarted { // 等待旧任务完全结束(可选) flaggedMapJob?.join() try { withContext(Dispatchers.Default) { mapMutex.withLock { // 先在Main线程清理旧标记 withContext(Dispatchers.Main) { selectedSiteMarker?.remove() selectedCircle?.remove() } // 处理坐标创建逻辑 val flaggedSiteMarkerLatLng = coordinateSiteLatitude?.let { lat -> coordinateSiteLongitude?.let { lng -> LatLng(lat, lng) } } // 添加新标记 flaggedSiteMarkerLatLng?.let { safeLatLng -> val selectedSiteOptions = MarkerOptions() .position(safeLatLng) .anchor(0.5f,0.5f) .visible(flaggedMarkerState) .flat(true) .zIndex(10f) .title(setTicketNumber(ticketNumber)) .snippet(appointmentName?:"Name is missing") .icon(vSelectedSiteIcon) withContext(Dispatchers.Main) { selectedSiteMarker = googleMap.addMarker(selectedSiteOptions)?.also{ it.tag = siteId } } } } } } catch (e: CancellationException) { // 协程被取消,这里可以加日志,不用处理锁 } } } ?: run { // 无数据时启动清理协程 lifecycleScope.launchWhenStarted { mapMutex.withLock { withContext(Dispatchers.Main) { selectedSiteMarker?.remove() selectedCircle?.remove() } } } } } liveDataMapListFromFiltered2?.observe(viewLifeCycleOwner) { mapDetailList -> filteredMapJob?.cancel() filteredMapJob = lifecycleScope.launchWhenStarted { filteredMapJob?.join() withContext(Dispatchers.Default) { // 耗时的列表处理操作 } // 多次安全操作Map mapMutex.withLock { withContext(Dispatchers.Main) { // 移动相机等操作 } } // 其他非Map操作 mapMutex.withLock { withContext(Dispatchers.Main) { // 其他Map操作 } } } } } } }
额外注意事项
- 尽量缩短锁持有时间:不要在
withLock()里执行长时间的非Map操作,比如你的坐标计算如果不涉及Map,可以放到withLock外面,避免其他协程长时间等待锁。 launchWhenStarted的生命周期特性:这个协程会在Fragment进入Started状态时恢复,暂停时挂起,但手动cancel()会直接终止它,不受生命周期影响。- 不要忽略
CancellationException:虽然不需要专门处理,但不要用catch(Exception)来捕获它,否则会掩盖协程取消的信号。
内容的提问来源于stack exchange,提问作者Roar Grønmo
相关产品推荐
相关产品推荐

