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

Kotlin协程活跃请求缓存实现中Deferred.isCompleted偶为false问题咨询

问题原因分析

你的推导逻辑存在两个核心错误:

  1. 默认await执行完成后才会进入finally,忽略了协程取消的场景:如果调用get方法的父协程被主动取消,request.await()会立即响应取消信号抛出CancellationException,直接进入finally块,此时后台执行的async任务尚未结束,isCompleted自然返回false。
  2. 忽略了并发场景的竞态问题:你使用的普通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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 17:06:01