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

多协程操作受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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 15:37:41