线程池的性能瓶颈

虽然线程池(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>
  • <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) // 主线程等待,以便协程有机会执行
}

不推荐使用

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐