1. 概述
在本篇文章中,我们将探讨如何并发执行一组任务,并等待它们全部完成。与传统的线程相比,Kotlin 推荐使用协程(coroutines)来实现并发编程,它更轻量、更高效,也更容易管理。
我们重点讨论两种常用模式:async-await
和 launch-join
,并结合结构化并发(Structured Concurrency)的最佳实践,避免常见的“协程泄漏”或“任务未等待”这类踩坑问题。
✅ 推荐阅读:Kotlin 中的线程 vs 协程
2. async-await 模式
Kotlin 的 async
函数用于启动一个可并发执行的协程,返回类型是 Deferred<T>
—— 可以理解为一个非阻塞、可取消的“未来结果”(类似 Java 中的 Future
)。通过调用 await()
方法,我们可以挂起当前协程,直到结果就绪。
更实用的是,当有多个 Deferred
任务时,可以使用 awaitAll()
扩展函数,等待所有任务完成:
@Test
fun whenAwaitAsyncCoroutines_thenAllTerminated() {
val count = AtomicInteger()
runBlocking {
val tasks = listOf(
async(Dispatchers.IO) { count.addAndGet(longRunningTask()) },
async(Dispatchers.IO) { count.addAndGet(longRunningTask()) }
)
tasks.awaitAll()
Assertions.assertEquals(2, count.get())
}
}
📌 注意:
- 这里使用了
Dispatchers.IO
,适用于 IO 密集型任务。 - ⚠️ 如果不显式指定
Dispatcher
,async
将继承父协程的调度器(即coroutineContext
中的 Dispatcher)。
2.1 利用结构化并发优化 async 使用
上面的例子虽然能工作,但手动管理 awaitAll()
容易出错,比如忘记调用就会导致任务“被丢弃”。
更好的方式是借助 结构化并发(Structured Concurrency)机制。其核心思想是:协程的生命周期受其所属的 CoroutineScope
管控,子协程未完成前,父作用域不会结束,从而防止协程泄漏。
✅ 我们可以通过 withContext
、coroutineScope
等构建器创建一个作用域,自动等待所有子协程完成,无需手动 awaitAll
:
@Test
fun whenParentCoroutineRunAsyncCoroutines_thenAllTerminated() {
val count = AtomicInteger()
runBlocking {
withContext(coroutineContext) {
async(Dispatchers.IO) { count.addAndGet(longRunningTask()) }
async(Dispatchers.IO) { count.addAndGet(longRunningTask()) }
}
Assertions.assertEquals(2, count.get())
}
}
📌 关键点:
withContext(coroutineContext)
创建了一个继承当前上下文的作用域。- 该作用域会等待内部所有协程(包括
async
启动的)完成后再退出。 - 因此断言执行时,两个任务已确定完成,无需显式
await
。
🔗 相关阅读:withContext 与 async/await 对比
其他作用域构建器对比
构建器 | 是否挂起 | 用途 |
---|---|---|
runBlocking |
❌ 阻塞线程 | 主要用于测试或桥接阻塞/非阻塞代码 |
coroutineScope |
✅ 挂起(非阻塞) | 用于并发等待多个子协程,常用于 suspend 函数内部 |
withContext |
✅ 挂起 | 切换调度器并执行代码块,返回结果 |
⚠️ 特别注意:runBlocking
会阻塞当前线程,而 coroutineScope
只是挂起,不会阻塞线程,这是本质区别。
3. launch-join 模式
当我们不需要协程返回值时,应优先使用 launch
而不是 async
。launch
返回一个 Job
对象,表示一个后台任务。通过调用 job.join()
可以等待其完成。
对于多个任务,Kotlin 提供了 joinAll()
扩展函数:
@Test
fun whenJoinLaunchedCoroutines_thenAllTerminated() {
val count = AtomicInteger()
runBlocking {
val tasks = listOf(
launch(Dispatchers.IO) { count.addAndGet(longRunningTask()) },
launch(Dispatchers.IO) { count.addAndGet(longRunningTask()) }
)
tasks.joinAll()
Assertions.assertEquals(2, count.get())
}
}
同样,为了利用结构化并发的优势,我们可以将 launch
放在父协程作用域内,避免手动管理生命周期:
@Test
fun whenParentCoroutineLaunchCoroutines_thenAllTerminated() {
val count = AtomicInteger()
runBlocking {
withContext(coroutineContext) {
launch(Dispatchers.IO) { count.addAndGet(longRunningTask()) }
launch(Dispatchers.IO) { count.addAndGet(longRunningTask()) }
}
Assertions.assertEquals(2, count.get())
}
}
✅ 优势:
- 不需要收集
Job
列表。 - 自动等待所有子任务完成。
- 更安全,不易遗漏
join
或await
。
4. 总结
本文介绍了在 Kotlin 协程中等待多个并发任务完成的两种主流方式:
- ✅ 使用
async + awaitAll
:适合需要返回值的并发场景。 - ✅ 使用
launch + joinAll
:适合仅需执行副作用(如更新状态)的任务。 - ✅ 更推荐结合
withContext
或coroutineScope
实现结构化并发,避免资源泄漏和逻辑错误。
💡 最佳实践:优先使用结构化并发,让作用域自动管理子协程生命周期,而不是手动
await
或join
。
完整示例代码可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-concurrency