协程新手到高手:彻底搞懂 Kotlin 协程作用域&协程调度器
由于传统线程池在 I/O 密集型场景下存在性能瓶颈,无法通过增加线程数量来进一步提高任务吞吐量,因此 Kotlin 引入了一种更加高效的并发模型——Kotlin 协程。Kotlin 协程(Coroutine)是一种轻量级并发编程机制,它允许在少量线程上执行大量异步任务,提高程序的并发能力,同时避免传统多线程编程中的线程切换开销和资源浪费。
线程池的性能瓶颈
虽然线程池(Thread Pool) 可以复用线程,减少创建和销毁线程的开销,但在 I/O 密集型任务(如网络请求、数据库操作)中仍然存在性能瓶颈。
下面我们分析一下在8核心16线程的CPU上执行10000任务,分别采用1线程、8线程、16线程、32线程、64线程、128线程、1000线程测试对CPU利用率、内存消耗、吞吐量的影响,以此为依据分析线程池的性能瓶颈。
测试数据(仅供参考)

分析数据对于执行10000个 I/O 密集型任务,我们可以得出以下结论:
- 最佳线程数 ≈ “2 × 逻辑核心数”,此时综合性能最优,CPU利用率适中、内存消耗适中,任务吞吐量高;
- 小于最佳线程数(如 8 或 16 线程),会导致 CPU 资源浪费,无法充分利用并发能力;
- 大于最佳线程数(如 64 或 128 线程),线程切换开销增加,内存消耗大,但是吞吐量增长放缓,甚至可能下降;
- 极端线程数(如 1000 线程),导致线程调度繁忙,内存消耗过大(可能OOM),吞吐量下降,严重影响性能;
🚀线程池的性能瓶颈主要受限于 CPU 核心数和线程调度开销。在 I/O 密集型任务中,线程数 ≈ 2 倍逻辑核心数时,任务吞吐量达到最优,CPU 利用率和线程切换成本达到平衡。然而,在硬件条件固定的情况下,线程池的吞吐量存在上限,超过该最佳线程数后,即使增加线程数量,也无法进一步提高任务吞吐量,甚至可能因线程切换开销增加而降低整体性能。因此,在线程池吞吐量无法满足更大任务需求时,仅靠增加线程数量已经无效,只能扩展硬件资源(如增加服务器节点)来提升系统处理能力。
**协程基本概念**
由于传统线程池在 I/O 密集型场景下存在性能瓶颈,无法通过增加线程数量来进一步提高任务吞吐量,因此 Kotlin 引入了一种更加高效的并发模型——Kotlin 协程。Kotlin 协程(Coroutine)是一种轻量级并发编程机制,它允许在少量线程上执行大量异步任务,提高程序的并发能力,同时避免传统多线程编程中的线程切换开销和资源浪费。
📌 **协程为什么这么牛 **🐮🐮🐮
因为它并不是单纯依赖增加线程,而是专注于提高单个线程的利用率,让少量线程在相同时间内处理更多任务,从而提升任务吞吐量。这样,即使系统线程资源受限,依然可以高效地执行大规模并发任务。
协程是由 Kotlin 运行时管理的轻量级任务,它运行在线程之上,并由协程调度器(CoroutineDispatcher)决定在哪个线程上执行。与传统线程不同,协程可以在不阻塞线程的情况下挂起和恢复,从而提高线程的利用率。当协程挂起时,线程不会被阻塞,而是可以继续执行其他任务。被挂起的协程不会影响线程的正常执行,它的恢复由协程调度器决定,既可以在原来的线程上恢复,也可以在不同的线程上恢复执行。
👇 **总结关键点 **🐮🐮🐮
- 协程是由 Kotlin 运行时管理的轻量级任务,它运行在线程之上,一个线程可以执行多个协程;
- 协程并由协程调度器(CoroutineDispatcher)分配协程在哪个线程上执行;
- 协程可以在不阻塞线程的情况下挂起和恢复,当协程挂起时线程不会被阻塞,而是可以继续执行其他任务;
- 协程恢复由协程调度器决定,既可以在原来的线程上恢复,也可以在不同的线程上恢复执行;
- 协程中也可以创建子协程,子协程的调度级别和主协程是同级别的,都是由协程调度器来调度;
总之,Kotlin 协程(Coroutine)是一种轻量级并发编程机制,它允许在少量线程上执行大量异步任务,提高程序的并发能力,同时避免传统多线程编程中的线程切换开销和资源浪费。
runBlocking 协程作用域
在 Kotlin 中,协程默认不能在<font style="color:rgb(14, 14, 14);">main()</font> 直接运行,协程执行必须要有一个协程作用域(后面单独讲解)—— 协程作用域用于管理协程的生命周期,确保协程可以受控地启动、运行,并在作用域销毁时自动取消,避免资源泄漏。
- runBlocking 就是协程作用域,它能够创建一个协程环境,并阻塞
<font style="color:rgb(14, 14, 14);">main()</font>线程,直到其作用域内的所有协程执行完毕。 - runBlocking 同时也是一个协程,可以在
<font style="color:rgb(14, 14, 14);">runBlocking{}</font>执行其他代码;
📌**** 示例
fun main() {
//开启调试模式,以看到协程id
System.setProperty("kotlinx.coroutines.debug", "on")
println("主线程开启,线程名称: ${Thread.currentThread().name}")
runBlocking {
println("主协程运行,协程名称: ${Thread.currentThread().name}") //打印协程名称
}
println("主线程结束,线程名称: ${Thread.currentThread().name}")
}
- 输出
主线程开启,线程名称: main
主协程运行,协程名称: main @coroutine#1
主线程结束,线程名称: main
- 解释
System.setProperty("kotlinx.coroutines.debug", "on")开启调试模式,会在线程名后面显示@coroutine#X,帮助跟踪协程的执行情况;- 主协程是第一个创建的协程,ID为
@coroutine#1
📌** 将main函数变为阻塞式的协程作用域**
:::warning
如果 main 函数中除了协程没有其他代码,那么可以直接使用 runBlocking 作为 main 的协程作用域入口,使整个 main 函数成为一个阻塞式的协程作用域。
:::
- 示例:main函数中只有
**runBlocking{}**
fun main(){
runBlocking {
//...
}
}
- 等价于:
**fun main() = runBlocking {}**
fun main() = runBlocking {
//会让 main 函数变成一个阻塞的协程作用域
println("这里就式协程作用域") //打印协程名称
}
launch** 启动子协程**
📖**** 虽然 **<font style="color:rgb(14, 14, 14);">runBlocking</font>** 本身是一个协程,但是通常不会直接在 runBlocking协程内部执行耗时任务,其主要作用是提供一个协程作用域,确保在 **<font style="color:rgb(14, 14, 14);">runBlocking</font>** 作用域内启动的所有子协程都能执行完毕。
📌**** 示例:使用**<font style="color:rgb(14, 14, 14);">launch{}</font>**创建子协程
import kotlinx.coroutines.*
fun main() { // 仅作为协程作用域
System.setProperty("kotlinx.coroutines.debug", "on")
println("main线程开始")
runBlocking() {
println("runBlocking协程开始:${Thread.currentThread().name}")
launch { // 协程 A
delay(1000)
println("协程A 执行完成 ${Thread.currentThread().name}")
}
launch() { // 协程 B
delay(500)
println("协程B 执行完成 ${Thread.currentThread().name}")
}
}
println("runBlocking协程结束")
println("main线程结束")
}
- 输出
main线程开始
runBlocking协程开始:main @coroutine#1
协程B 执行完成 main @coroutine#3
协程A 执行完成 main @coroutine#2
runBlocking协程结束
main线程结束
- 解释
1. 进入main线程,输出 "main线程开始"
2. 进入runBlocking协程作用域,创建第一个主协程,同时main线程阻塞;
3. 主协程输出 "runBlocking协程开始:main @coroutine#1"
3. 启动子协程A,马上挂起1000毫秒,主协程继续往下执行
4. 启动协程B,马上挂起500毫秒,此时主协程没有可执行代码,需等其子协程执行恢复
5. 协程B恢复执行,输出 "协程B 执行完成 main @coroutine#3"
6. 协程A恢复执行,输出 "协程A 执行完成 main @coroutine#2"
7. 此时所有子协程执行完毕,输出 "runBlocking主协程结束"
8. main线程停止阻塞,输出 "main线程结束"
**suspend 挂起函数**
挂起函数(suspend function) 是 Kotlin 协程的核心概念,它允许函数在不阻塞线程的情况下暂停执行,并在合适时机恢复。
📌 **suspend** 仅仅用来标记一个函数支持挂起,它不会让函数自动挂起,只有遇到真正的挂起操作如 <font style="color:rgb(14, 14, 14);">delay()</font>时,才会暂停执行;‼️ 重要的是暂停执行时,线程没有阻塞;
import kotlinx.coroutines.*
fun main() = runBlocking {
println("调用前")
mySuspendFunction()
println("调用后")
}
suspend fun mySuspendFunction() {
delay(1000)
println("挂起函数执行完毕")
}
- 输出
调用前
(暂停1秒)
挂起函数执行完毕
调用后
⚠️** 如果一个****suspend**函数内部没有任何挂起点(即没有执行任何挂起操作,如**delay()**),则和普通函数没有区别;
async 启动子协程
<font style="color:rgb(14, 14, 14);">async</font> 是 Kotlin 协程提供的一种并发执行任务的方式,用于启动一个新的协程并返回 <font style="color:rgb(14, 14, 14);">Deferred<T></font> 对象,可以通过 <font style="color:rgb(14, 14, 14);">await() </font>获取异步计算的结果。
async 适用于:
- 需要并发执行多个任务,提高执行效率;
- 任务有返回值,需要获取执行结果;
- 避免阻塞线程,实现真正的异步并行计算;
📌 示例:启动 **<font style="color:rgb(14, 14, 14);">async</font>** 并获取返回值
import kotlinx.coroutines.*
//任务1
suspend fun task1(): Int {
delay(1000)
return 10
}
//任务2
suspend fun task2(): Int {
delay(1000)
return 20
}
//主线程
fun main() = runBlocking {
val result1 = async { task1() }
val result2 = async { task2() }
println("最终结果:${result1.await() + result2.await()}")
}
- 输出(总耗时 1 秒,而不是 2 秒)
(1 秒后)
最终结果:30
- 解释
<font style="color:rgb(14, 14, 14);">task1()</font>和<font style="color:rgb(14, 14, 14);">task2()</font>并行执行,因为<font style="color:rgb(14, 14, 14);">async</font>启动的协程不会阻塞 main 线程。<font style="color:rgb(14, 14, 14);">await()</font>等待所有任务完成后再继续执行。
协程作用域(**Coroutine Scope**)
协程作用域(Coroutine Scope)用于管理协程的生命周期,确保协程能够有序启动、运行和取消。它提供了一种结构化并发的方式,使协程能够在特定范围内运行,并且在范围结束时自动取消未完成的协程。
📌**** Kotlin提供了5种不同的协程作用域,如下图所示

✅ 推荐使用
**<font style="color:rgb(14, 14, 14);">coroutineScope{}</font>**: 挂起父协程,并创建一个新的作用域,等待其中的所有子协程执行完毕后才恢复父协程。**<font style="color:rgb(14, 14, 14);">supervisorScope{}</font>**:挂起父协程,并创建一个新的作用域,等待其中所有未失败的协程完成后才恢复父协程。**<font style="color:rgb(14, 14, 14);">CoroutineScope</font>**:不阻塞线程、不挂起父协程,创建一个新的作用域,不等待其中子协程是否完成,不因子协程失败而取消整个协程域;
❌ 不推荐使用
**<font style="color:rgb(14, 14, 14);">runBlocking{}</font>**:会阻塞线程,挂起父协程,创建新作用域,等待其中所有子协程执行完毕后恢复父协程;**<font style="color:rgb(14, 14, 14);">GlobalScope</font>**:生命周期难管理,容易导致资源泄露,不建议用于一般业务逻辑。
**coroutineScope **🏆
📖 <font style="color:rgb(14, 14, 14);">coroutineScope</font> 挂起父协程,并创建一个新的作用域,确保其中的所有子协程执行完毕后才恢复父协程。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("主协程开始")
launch {
println("父协程开始")
coroutineScope { // 挂起父协程,同时创建新的协程作用域
launch {
delay(1000)
println("子协程 1 执行完毕")
}
launch {
delay(5000)
println("子协程 2 执行完毕")
}
}
println("父协程结束") // 只有 coroutineScope 内的协程执行完毕后才会执行
}
println("主协程结束") // 由于 runBlocking,它最后才执行
}
🏃 输出
主协程开始
主协程结束
父协程开始
子协程 1 执行完毕
子协程 2 执行完毕
父协程结束
👄 解释
<font style="color:rgb(14, 14, 14);">runBlocking</font>协程启动,输出<font style="color:rgb(14, 14, 14);">"主协程开始"</font>;<font style="color:rgb(14, 14, 14);">launch{}</font>启动子协程,但不阻塞<font style="color:rgb(14, 14, 14);">runBlocking</font>协程,<font style="color:rgb(14, 14, 14);">runBlocking</font>继续执行,输出<font style="color:rgb(14, 14, 14);"> "父协程结束"</font>;<font style="color:rgb(14, 14, 14);">launch{}</font>输出<font style="color:rgb(14, 14, 14);">"父协程开始"</font>- 然后
<font style="color:rgb(14, 14, 14);">coroutineScope{}</font>创建新作用域,同时挂起父协程,确保其所有子协程执行完毕- 1秒后,输出
<font style="color:rgb(14, 14, 14);">"子协程 1 执行完毕"</font> - 5秒后,输出
<font style="color:rgb(14, 14, 14);">"子协程 2 执行完毕"</font>
- 1秒后,输出
<font style="color:rgb(14, 14, 14);">coroutineScope{}</font>执行完毕,恢复其父协程继续执行,输出<font style="color:rgb(14, 14, 14);">"父协程结束"</font>
💡**** 还可以将<font style="color:rgb(14, 14, 14);">coroutineScope{}</font>作用域中的代码抽取都一个<font style="color:rgb(14, 14, 14);">suspend</font>函数中,调用此函数的协程将被挂起,等<font style="color:rgb(14, 14, 14);">suspend</font>执行完毕后再恢复执行。
package com.example.myapplication
import kotlinx.coroutines.*
fun main() = runBlocking {
println("主协程开始")
launch {
println("父协程开始")
doWork() //函数内所有协程执行完毕后,才会继续往下执行
println("父协程结束")
}
delay(1500)
println("主协程结束") // 由于 runBlocking,它最后才执行
}
//调用者要等待函数内所有协程执行完毕后,才能继续执行
private suspend fun doWork() {
coroutineScope { // 挂起当前 launch 作用域
launch {
delay(1000)
println("子协程 1 执行完毕")
}
launch {
delay(500)
println("子协程 2 执行完毕")
}
}
}
🌟**** 如果一个函数中有且仅有只有一个**<font style="color:rgb(14, 14, 14);">coroutineScope{}</font>**可以简写为如下格式
//调用者要等待函数内所有协程执行完毕后,才能继续执行
suspend fun doWork() = coroutineScope {
launch {
delay(1000)
println("子协程 1 执行完毕")
}
launch {
delay(500)
println("子协程 2 执行完毕")
}
}
supervisorScope 🏆
📖<font style="color:rgb(14, 14, 14);"> supervisorScope{} </font>和 <font style="color:rgb(14, 14, 14);">coroutineScope{}</font> 特点大体相同,它也会挂起父协程,并创建一个新的作用域,确保其中的所有子协程执行完毕后才恢复父协程。
和**<font style="color:#ED740C;">coroutineScope{}</font>**不会因某个子协程的失败而取消整个作用域,只会取消失败的协程,其他协程仍然正常执行
import kotlinx.coroutines.*
fun main() = runBlocking {
supervisorScope {
launch {
delay(500)
println("子协程 1 执行完成")
}
launch {
delay(300)
throw RuntimeException("子协程 2 发生异常")
}
launch {
delay(700)
println("子协程 3 执行完成")
}
}
println("supervisorScope 结束")
}
🏃 输出
子协程 2 发生异常,并不影响supervisorScope{}域的其他协程继续执行。
Exception in thread "main" java.lang.RuntimeException: 子协程 2 发生异常
at AKt$main$1$1$2.invokeSuspend(A.kt:12)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:233)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:595)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:493)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at AKt.main(A.kt:3)
at AKt.main(A.kt)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@31f924f5, BlockingEventLoop@5579bb86]
子协程 1 执行完成
子协程 3 执行完成
supervisorScope 结束
Process finished with exit code 0
CoroutineScope 🏆
📖 CoroutineScope 提供可管理的协程作用域,在作用域被取消时,其所有子协程也会被取消。它不会挂起调用者(不会阻塞线程、也不会让父协程等待),适用于需要手动管理生命周期的场景。
import kotlinx.coroutines.*
fun main() {
//创建自定义协程作用域
val scope = CoroutineScope(Dispatchers.Default)
//在此作用域上启动协程(不阻塞主线程)
scope.launch{
delay(1000)
println("CoroutineScope 作用域中的协程执行完成")
}
//让主线程等待,否则如果主线程结束了,而CoroutineScope作用域下协程恢来不急执行;
Thread.sleep(2000)
println("主线程执行完毕")
}
✅**** 不会阻塞线程,适用于实际应用开发。
runBlocking 🙅
✍️ runBlocking 会阻塞主线程,直到其中的所有协程执行完毕,仅推荐用于测试和main函数中使用;
import kotlinx.coroutines.*
fun main() {
runBlocking {
launch {
delay(1000L)
println("runBlocking 作用域中的协程执行完成")
}
}
println("主线程执行结束")
}
❌**** 不适用与实际异步编程
GlobalScope 🙅
✍️ GlobalScope作用域创建的协程不受限于任何特定的作用域,一旦启动,它将独立于 main 函数或其他作用域执行,生命周期与程序进程一样长,容器导致内存泄漏;
package com.example.myapplication
import kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
fun main() {
//启动一个全局作用域协程,该协程独立于main线程,生命周期与进程一样长
GlobalScope.launch {
delay(1000L)
println("GlobalScope 协程执行完成")
}
Thread.sleep(1500L) // 主线程等待,以便协程有机会执行
}
❌ 不推荐使用
更多推荐
所有评论(0)