Kotlin协程活跃请求缓存实现中Deferred.isCompleted偶为false问题咨询
问题原因分析
你的推导逻辑存在两个核心错误:
- 默认
await执行完成后才会进入finally,忽略了协程取消的场景:如果调用get方法的父协程被主动取消,request.await()会立即响应取消信号抛出CancellationException,直接进入finally块,此时后台执行的async任务尚未结束,isCompleted自然返回false。 - 忽略了并发场景的竞态问题:你使用的普通
MutableMap本身线程不安全,当多个协程同时调用相同RequestBody对应的get方法时,可能出现前一个请求刚执行到finally的remove逻辑,后一个相同key的请求已经把新的未完成Deferred存入了Map,此时remove拿到的是后一个未完成的任务,也会返回isCompleted=false。
正确实现方案
需要从线程安全、并发竞态规避、取消隔离三个维度调整实现:
- 缓存容器改用线程安全的
ConcurrentHashMap,避免并发读写异常 - 使用原子操作
putIfAbsent、remove(key, value)规避相同key的任务误删/重复创建问题 - 隔离请求任务和调用方的生命周期,避免调用方取消打断正在执行的请求
具体实现代码如下:
// 改用线程安全的ConcurrentHashMap private val requestJobCache: MutableMap<RequestBody, Deferred<Response>> = ConcurrentHashMap() suspend fun fetch(body: RequestBody): Response { // ... 原有请求逻辑 } suspend fun get(body: RequestBody): Response { // 第一层无锁快速校验,提升性能 requestJobCache[body]?.let { return it.await() } return coroutineScope { // 懒启动async,避免未拿到缓存写入权时浪费资源创建任务 val request = async(start = CoroutineStart.LAZY) { fetch(body) } // 原子操作存入缓存,避免重复创建相同请求 val existingRequest = requestJobCache.putIfAbsent(body, request) if (existingRequest != null) { // 已有其他协程创建了相同请求,直接复用 existingRequest.await() } else { try { request.start() request.await() } finally { // 仅删除当前协程自己创建的请求任务,避免误删其他协程刚存入的同key任务 requestJobCache.remove(body, request) // 此时request.isCompleted必然为true } } } }
该实现中
remove(key, value)的重载方法只会在Map中当前key对应的value正好是传入的value时才执行删除,完全避免了并发竞态问题。同时请求任务和调用方取消逻辑隔离,调用方取消只会终止自身的等待逻辑,不会打断正在执行的全局请求任务,保证finally执行时当前任务必然已完成。
内容的提问来源于stack exchange,提问作者dadam
相关产品推荐
相关产品推荐

