请求详解Kotlin协程内部工作机制、资源利用及比线程更耗资源的原因
一、先从基础概念搭起(适配Kotlin零基础)
先明确两个核心概念:
- 系统线程:是操作系统内核管理的执行单元,每个线程有独立的栈空间(默认几MB),线程的创建、切换、销毁都需要内核参与,开销很大,系统能同时运行的线程数量有限(一般几十到上百个)。
- 协程:是Kotlin提供的用户态轻量级执行单元,它依赖线程运行,但本身不是线程。你可以把协程理解成"跑在线程上的任务片段",多个协程可以复用同一个线程。
二、协程的内部工作机制
1. 调度器(Dispatcher):协程的"分配器"
协程不会自己跑,需要调度器把它分配到线程上执行。常用的调度器有:
Dispatchers.Default:用于CPU密集型任务,复用一个默认的线程池(线程数等于CPU核心数)Dispatchers.IO:用于IO密集型任务(比如网络请求、文件读写),复用一个可扩容的线程池Dispatchers.Main:Android等平台的主线程调度器,用来更新UI
调度器的核心作用是线程复用:当一个协程进入"挂起"状态时,调度器会把当前线程分配给其他等待执行的协程,不会让线程空闲。
2. 挂起与恢复:协程的核心能力
这是协程和线程最大的区别:
- 线程的阻塞:比如调用
Thread.sleep(1000),线程会被内核挂起,这段时间线程什么都干不了,完全占用系统资源。 - 协程的挂起:当你调用
delay(1000)这种挂起函数时,协程会把自己的执行状态(比如当前变量值、执行到哪一行)保存起来,然后主动让出线程。等1秒到了,调度器会找个空闲的线程,把协程的状态恢复,接着从挂起的地方继续执行。
简单说:挂起不是阻塞线程,而是协程"暂时退场",让线程去干别的活。
3. 协程上下文(CoroutineContext):协程的运行环境
每个协程都有自己的上下文,包含:
- 调度器:决定协程跑在哪个线程
- Job:用来管理协程的生命周期(比如取消协程、等待协程完成)
- 其他元素:比如异常处理器等
三、协程的资源利用方式
1. 内存占用极低
单个协程的栈空间只有几KB(可以动态扩容),而线程的栈默认是1-2MB。这意味着:
- 你可以同时运行十万级别的协程,内存占用也不会很高
- 但如果是线程,运行几百个就会把系统内存占满,甚至崩溃
2. 线程利用率极高
因为协程挂起时会让出线程,线程不会空闲。比如IO调度器的线程池,当1000个协程都在等待网络请求时,线程池里的几个线程可以被其他协程复用,而不是每个协程都占一个线程等着。
四、关于"协程比线程更消耗资源"的误解
协程本身比线程节省资源,你产生这种错觉大概率是因为错误的使用方式:
1. 在协程里调用阻塞式方法
比如在协程里用Thread.sleep(1000)而不是delay(1000):
runBlocking { launch { // 这会阻塞当前线程,协程无法挂起,线程被占死 Thread.sleep(1000) } }
这种情况下,协程无法让出线程,导致线程池里的线程被占用,无法复用,反而比直接用线程更浪费资源。正确的做法是用挂起函数替代阻塞方法。
2. 协程创建后未及时取消
如果创建了大量协程但没在不需要的时候取消,虽然单个协程内存小,但数量极多(比如百万级)时,也会有一定的内存消耗。但这种情况的消耗远低于同数量的线程。
3. 混淆了切换开销
协程的切换是用户态的,不需要内核参与,开销只有线程切换的几十分之一。但如果频繁切换协程(比如每秒切换几十万次),可能会有一些开销,但这属于极端场景,日常开发中几乎遇不到。
举个直观的对比例子
线程的极限
运行这段代码,系统大概率会崩溃或者无响应,因为创建了1000个系统线程,资源占用过高:
fun main() { repeat(1000) { Thread { Thread.sleep(1000) println("Thread $it finished") }.start() } }
协程的优势
运行这段代码,10000个协程可以轻松执行,内存占用极低:
fun main() = runBlocking { repeat(10000) { launch { delay(1000) println("Coroutine $it finished") } } }
内容的提问来源于stack exchange,提问作者somerandomname

