1. 概述

在本篇文章中,我们将探讨如何并发执行一组任务,并等待它们全部完成。与传统的线程相比,Kotlin 推荐使用协程(coroutines)来实现并发编程,它更轻量、更高效,也更容易管理。

我们重点讨论两种常用模式:async-awaitlaunch-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 密集型任务。
  • ⚠️ 如果不显式指定 Dispatcherasync 将继承父协程的调度器(即 coroutineContext 中的 Dispatcher)。

2.1 利用结构化并发优化 async 使用

上面的例子虽然能工作,但手动管理 awaitAll() 容易出错,比如忘记调用就会导致任务“被丢弃”。

更好的方式是借助 结构化并发(Structured Concurrency)机制。其核心思想是:协程的生命周期受其所属的 CoroutineScope 管控,子协程未完成前,父作用域不会结束,从而防止协程泄漏。

✅ 我们可以通过 withContextcoroutineScope 等构建器创建一个作用域,自动等待所有子协程完成,无需手动 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 而不是 asynclaunch 返回一个 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 列表。
  • 自动等待所有子任务完成。
  • 更安全,不易遗漏 joinawait

4. 总结

本文介绍了在 Kotlin 协程中等待多个并发任务完成的两种主流方式:

  • ✅ 使用 async + awaitAll:适合需要返回值的并发场景。
  • ✅ 使用 launch + joinAll:适合仅需执行副作用(如更新状态)的任务。
  • ✅ 更推荐结合 withContextcoroutineScope 实现结构化并发,避免资源泄漏和逻辑错误。

💡 最佳实践:优先使用结构化并发,让作用域自动管理子协程生命周期,而不是手动 awaitjoin

完整示例代码可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-concurrency


原始标题:Kotlin Coroutines: Waiting for Multiple Threads to Finish