kotlin协程 容易被忽视的CompletableDeferred

CompletableDeferred是一个 可手动完成 的 Deferred, 它实现了 Deferred(可以 await()),也提供了 complete(value) / completeExceptionally(e) / cancel() 等方法,并由外部触发结果。它创建后可能处于未完成状态,任意线程或协程都能把它完成或失败,等待方使用 await() 来获取结果。

一、与Deferred、suspendCancellableCoroutine和Channel的区别

  • vs Deferred(由 async 返回):async 的 Deferred 是由协程体自己完成;CompletableDeferred 允许外部完成。

  • vs suspendCancellableCoroutine:两者都能把回调桥接到协程,suspendCancellableCoroutine 适合一次性封装回调;CompletableDeferred 更适合"外部多方在不同时间完成/通知"的场景(比如事件总线、跨协程信号、测试用的手动完成)。

  • vs Channel:Channel 是多次消息传递而CompletableDeferred 是单次结果(一次性)。

二、经典应用场景

1、桥接回调

一般的桥接回调推荐用suspendCancellableCoroutine,但如果外部多方的桥接推荐用CompletableDeferred。下面是一个简单的网络请求的例子。

kotlin 复制代码
suspend fun Call.awaitResponse(): Response = suspendCancellableCoroutine { cont ->
    enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            if (cont.isActive) cont.resumeWithException(e)
        }

        override fun onResponse(call: Call, response: Response) {
            //response记得use{}关闭
            if (cont.isActive) cont.resume(response) 
        }
    })
    cont.invokeOnCancellation { cancel() }
}

如果你想用 CompletableDeferred(当回调可能多次触发或在别处完成时非常好):

kotlin 复制代码
fun makeRequest(): CompletableDeferred<String> {
    val deferred = CompletableDeferred<String>()
    httpClient.newCall(request).enqueue(object: Callback {
        override fun onFailure(call: Call, e: IOException) {
            deferred.completeExceptionally(e)
        }
        override fun onResponse(call: Call, response: Response) {
            val s = response.body?.string() ?: ""
            deferred.complete(s)
            response.close()
        }
    })
    return deferred
}

// 使用处
lifecycleScope.launch {
    try {
        val result = makeRequest().await()
    } catch (e: Throwable) { ... }
}

上面的桥接肯定推荐suspendCancellableCoroutine来完成,只是为了展示CompletableDeferred也可以完成回调的桥接。下面看一个多处回调桥接的例子:

kotlin 复制代码
// 使用 CompletableDeferred 桥接多回调
suspend fun wakeUpDevice(): Boolean = coroutineScope {
    val deferred = CompletableDeferred<Boolean>()
    val finished = AtomicBoolean(false)
    val failCount = AtomicInteger(0)

    // 本地唤醒
    sendLocalWakeUp(
        onSuccess = {
            if (finished.compareAndSet(false, true)) {
                deferred.complete(true)
            }
        },

        onFail = {
            if (failCount.incrementAndGet() == 2) {
                deferred.complete(false)
            }
        }
    )

    // 云端唤醒
    sendCloudWakeUp(
        onSuccess = {
            if (finished.compareAndSet(false, true)) {
                deferred.complete(true)
            }
        },

        onFail = {
            if (failCount.incrementAndGet() == 2) {  
                deferred.complete(false)            
            }
        }
    )

    // 统一 suspend 等待结果
    deferred.await()
}

  


// 示例调用
fun main() = runBlocking {
    val result = wakeUpDevice()
    println("唤醒结果:$result")
}

上面就是多处回调桥接的例子,只要有一次成功就成功,都失败才算失败。

2、多生产者单消费者"谁先到就用谁"

complete函数只能被调用一次,后续调用无效,基于这个特性可以完成多生产者单消费者-谁先到就用谁的需求。一个简单的示例如下:

kotlin 复制代码
//两个异步来源(A、B)谁先返回就用谁的结果
val winner = CompletableDeferred<String>()
fun sourceA() { /* 异步获取 */ winner.complete("fromA") }
fun sourceB() { /* 异步获取 */ winner.complete("fromB") }
lifecycleScope.launch {
    val result = winner.await() // 谁先 complete 就是结果
}

3、协程间通知

这是一个比较巧妙的用法,例如一个协程做准备工作,另一个协程等待完成:

kotlin 复制代码
val ready = CompletableDeferred<Unit>()

launch {
    prepareResources()
    ready.complete(Unit)
}

launch {
    ready.await() //挂起阻塞,等待准备(prepareResources)完成
    startWork()  //另一个协程的执行部分
}

扩展思维:ready.complete(Unit)还可以在用户输入后,点击按钮后等,可以发挥想象。

4、与select、超时结合

select 表达式同时等待多个挂起操作采用第一个完成的结果。

kotlin 复制代码
fun CoroutineScope.fetchWithFallbacks(): CompletableDeferred<String> {
    val result = CompletableDeferred<String>()

    launch {
        try {
            val data = select<String> {
                primarySource().onAwait { it }
                secondarySource().onAwait { it }
                onTimeout(800) { throw TimeoutException("Timeout") }
            }
            result.complete(data)
        } catch (e: Exception) {
            // 启动异步恢复任务(不立即completeExceptionally)
            launch {
                delay(1000)
                try {
                    val recovery = recoverySource()
                    result.complete(recovery)
                } catch (e2: Exception) {
                    result.completeExceptionally(e2) // 真正失败
                }
            }
        }
    }

    return result
}

三、取消与异常传播

  • 如果 CompletableDeferred 被 cancel(),等待的协程会收到 CancellationException。

  • 如果在完成前等待者被取消,完成者仍然可以 complete() ------ 但 await() 不会再返回值(因为等待者已被取消)。

  • completeExceptionally(e) 会使 await() 抛出相同异常。

  • complete() 返回 true 或 false(表示是否第一次完成)。

四、注意事项与常见坑

  1. 不要泄漏未完成的 CompletableDeferred:把它长期暴露给全局可能会导致内存泄漏,尤其当负责完成它的组件被销毁时。

  2. 只在受信任的地方完成:不要让多个地方都可能完成且没序的代码混乱;若有竞态,记得检查 complete 的返回值。

  3. 避免把 CompletableDeferred 当作长生命周期队列:它是一次性完成的;若需要多次事件,请用 Channel / SharedFlow。

  4. 取消处理:如果等待者可能取消,使用 invokeOnCompletion 或 invokeOnCancellation 做清理(比如取消底层请求)。

  5. 完成后不要反复设置结果:complete 多次调用只有第一次有效,后续会返回 false。

  6. 与协程作用域的关系:CompletableDeferred 本身不是 Job,但它有 asJob() 可用于组合;也可以传入父 Job 构造(CompletableDeferred(parentJob))以便取消联动。

相关推荐
czhc11400756636 小时前
Linux1023 mysql 修改密码等
android·mysql·adb
GOATLong7 小时前
MySQL内置函数
android·数据库·c++·vscode·mysql
onthewaying8 小时前
Android SurfaceTexture 深度解析
android·opengl
茄子凉心8 小时前
Android Bluetooth 蓝牙通信
android·蓝牙通信·bluetooth通信
00后程序员张9 小时前
iOS 26 App 运行状况全面解析 多工具协同监控与调试实战指南
android·ios·小程序·https·uni-app·iphone·webview
2501_9160074710 小时前
iOS 混淆实战,多工具组合完成 IPA 混淆、加固与发布治理(iOS混淆|IPA加固|无源码混淆|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
2501_9159184110 小时前
怎么上架 App?iOS 应用上架完整流程详解与跨平台发布实战指南
android·ios·小程序·https·uni-app·iphone·webview
2501_9291576810 小时前
【安卓+PC+IOS】psp全中文游戏+高清纹理包+金手指
android·游戏·ios
2501_9160088910 小时前
iOS 混淆工具链实战 多工具组合完成 IPA 混淆与加固(iOS混淆|IPA加固|无源码加固|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview