创建协程和创建线程
虽然 Kotlin 运行在 JVM 上,但我们可以使用 Kotlin 原生方式或协程机制来实现类似线程的行为,而无需直接调用 Java 的 Thread
API。
使用 Kotlin 协程
协程是 Kotlin 的轻量级并发解决方案,它能更高效地利用线程资源:
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 启动协程(在默认线程池中执行)
val job = launch(Dispatchers.Default) {
println("协程在后台线程执行: ${Thread.currentThread().name}")
// 模拟耗时操作
delay(1000)
println("任务完成")
}
println("主线程继续执行: ${Thread.currentThread().name}")
job.join() // 等待协程完成
}
协程的优点:
- 轻量级(数万个协程 vs 数千个线程)
- 结构化并发模型
- 可挂起/恢复的操作
- 内置取消支持
- 高效的资源利用
使用kotlin标准库API创建线程
Kotlin 标准库提供了简化线程操作的扩展:
kotlin
import kotlin.concurrent.thread
fun main() {
println("主线程开始: ${Thread.currentThread().name}")
// 创建并启动新线程(kotlin.concurrent.thread)
val myThread = thread(start = false, name = "KotlinThread") {
println("新线程执行: ${Thread.currentThread().name}")
Thread.sleep(1000)
println("线程任务完成")
}
// 配置线程属性
myThread.isDaemon = true
myThread.priority = Thread.NORM_PRIORITY
// 启动线程
myThread.start()
println("主线程等待子线程...")
myThread.join()
println("主线程结束")
}
特点:
thread()
是 Kotlin 标准库函数(kotlin.concurrent 包)- 底层仍使用 Java Thread,但 API 完全 Kotlin 化
- 支持命名线程、配置守护线程状态等
完全使用 Kotlin/Native(非 JVM 环境)
在 Kotlin/Native 环境下可使用平台特定 API:
kotlin
import kotlin.native.concurrent.*
fun main() {
// 创建工作线程
val worker = Worker.start()
// 提交任务
val future = worker.execute(TransferMode.SAFE, { Unit }) {
println("在Native工作线程执行")
// 模拟耗时工作
repeat(1000_000) { it * it }
}
// 等待结果
future.consume { println("任务完成") }
// 请求停止
worker.requestTermination().result // 等待线程终止
}
方法对比表
方法 | 是否需要 JVM | 轻量级 | 复杂性 | 适用场景 |
---|---|---|---|---|
协程 (Dispatchers.Default) | ✓ | ★★★★☆ | ★★☆☆☆ | 多数并发任务,I/O 操作 |
Kotlin thread() | ✓ | ★★☆☆☆ | ★★★☆☆ | 需要显式线程控制的场景 |
协程 + 自定义调度器 | ✓ | ★★★★☆ | ★★★☆☆ | 需要特定线程行为的场景 |
Kotlin/Native Worker | ✗ | ★★★★☆ | ★★★★☆ | 原生应用、跨平台开发 |
最佳实践
-
首选协程:现代Kotlin应用的标准并发解决方案
kotlinCoroutineScope(Dispatchers.Default).launch { // 并发操作 }
-
谨慎创建线程:线程资源昂贵(仅当必要且理解成本时)
kotlinthread { // 确保这是必要的 }.apply { name = "CustomThread" isDaemon = true // 避免阻止JVM关闭 }
-
资源管理:确保正确关闭线程/协程作用域
kotlinval scope = CoroutineScope(newSingleThreadContext("MyThread")) scope.cancel() // 完成后取消避免泄漏
-
避免共享状态:使用协程通道或线程安全数据结构
kotlinval channel = Channel<Data>() // 生产者 launch { while (true) channel.send(generateData()) } // 消费者 launch { for (data in channel) process(data) }
线程创建与切换的成本分析:为何协程更高效
线程的创建成本(耗时点)
1. 内核对象创建(核心耗时点)
-
每次创建新线程时,操作系统必须在内核空间创建:
- 线程控制块(Thread Control Block, TCB)
- 堆栈空间(默认大小通常为 1-2MB)
- 安全描述符(Security Descriptor)
-
示例测试(在标准 Linux 系统上):
kotlinfun main() { val count = 1000 val start = System.nanoTime() val threads = List(count) { thread(isDaemon = true) { Thread.sleep(10) } } threads.forEach { it.join() } val duration = (System.nanoTime() - start) / 1e6 println("创建$count个线程耗时: ${"%.2f".format(duration)}ms") }
结果:创建 1000 个线程耗时约 1200-2000ms
2. 内存分配开销
-
每个线程的固定开销:
- 堆栈内存:默认 1MB(可通过
-Xss
调整,但最小约 64KB) - 内核数据结构:约 2-4KB
- JVM 内部结构:约 1-2KB
- 堆栈内存:默认 1MB(可通过
-
总计:每个线程约 1MB+ 的内存开销
3. 上下文切换成本
css
graph LR
A[运行中线程] -->|1. 保存寄存器状态| B[CPU 缓存]
B -->|2. 更新内存管理单元| C[新线程堆栈]
C -->|3. 加载新线程状态| D[新线程执行]
D -->|4. 恢复执行环境| E[新线程运行]
耗时操作:
- 保存/恢复 CPU 寄存器状态(约 100-200 纳秒)
- 缓存失效(Cache Invalidation):L1/L2 缓存需要重新加载
- TLB(页表缓存)刷新(约 1000 纳秒)
- 内核模式切换(用户态↔内核态,约 100-200 纳秒)
线程资源占用详情
1. 内存资源
组件 | 大小 | 说明 |
---|---|---|
用户堆栈 | 512KB-2MB | 函数调用、局部变量 |
内核堆栈 | 8-16KB | 系统调用使用 |
TCB | 1-4KB | 线程状态信息 |
TLS | 变量 | 线程本地存储 |
JVM 结构 | 1-2KB | Java 线程对象内部数据 |
2. CPU 资源
-
上下文切换频率:
- 每毫秒发生 1-10 次(取决于系统负载)
- 每次切换占用 1-5 微秒 CPU 时间
-
调度开销:
- 调度器决策时间(O(n) 或 O(1) 算法)
- 优先级计算
3. 系统资源
-
最大线程数限制:
/proc/sys/kernel/threads-max
(Linux)ulimit -u
(用户级限制)- 典型限制:单进程 1k-10k 线程
-
文件描述符:
- 每个线程可能打开文件/Socket
- 占用系统级文件描述符限制
线程的核心劣势
1. 可伸缩性差
kotlin
// 线程版 Web 服务器(伪代码)
class ThreadPerConnectionServer {
fun run() {
while (true) {
val socket = serverSocket.accept()
thread { // 为每个连接创建新线程
handleRequest(socket)
}
}
}
fun handleRequest(socket: Socket) {
// 处理请求(可能耗时)
}
}
// 协程版 Web 服务器
class CoroutineServer {
fun run() = runBlocking {
while (true) {
val socket = serverSocket.accept()
launch { // 使用协程替代线程
handleRequest(socket)
}
}
}
}
对比结果:
- 线程版:5000 连接 → 约 5GB 内存(仅线程开销)
- 协程版:5000 连接 → 约 5MB 内存(协程栈复用)
2. 上下文切换开销高
操作 | 线程开销 | 协程开销 |
---|---|---|
保存/恢复寄存器 | 100+ ns | <10 ns |
内核态切换 | 必须 | 不需要 |
缓存失效 | 严重 | 轻微 |
调度决策 | 内核调度器(复杂) | 用户态调度(简单) |
3. 同步成本昂贵
kotlin
// 线程间的同步
val lock = ReentrantLock()
fun threadWork() {
lock.lock() // 可能触发内核态切换
try {
// 临界区操作
} finally {
lock.unlock()
}
}
// 协程同步
val mutex = Mutex()
suspend fun coroutineWork() {
mutex.lock() // 挂起点,无内核切换
try {
// 临界区
} finally {
mutex.unlock()
}
}
同步开销对比:
- 无竞争锁:线程 > 20ns,协程 < 10ns
- 高竞争锁:线程可能 > 1000ns(等待队列+上下文切换),协程 < 100ns
4. 资源限制问题
css
graph TD
A[客户端请求] --> B{线程数<上限?}
B -->|是| C[创建新线程]
B -->|否| D[拒绝请求]
C --> E[服务请求]
- 系统限制:通常 1000-10000 线程/进程
- 资源耗尽:达到上限后无法接受新连接
- 解决方案:需使用线程池(但引入管理复杂度)
协程的优势与实现原理
协程轻量级的原因
1. 栈复用技术
kotlin
// 协程栈分配原理
class CoroutineStack(size: Int) {
private val buffer = ByteArray(size) // 共享内存池
fun allocate(): StackFrame {
// 从池中分配片段
return StackFrame(buffer, current, size)
}
fun free(frame: StackFrame) {
// 返还内存池
}
}
- 每个协程初始栈:几百字节(约 2KB)
- 按需增长(通过拷贝)
- 结束后内存返回池中
2. 用户态调度
kotlin
// 简化的协程调度器
class CoroutineScheduler(threadCount: Int) {
private val queues = Array(threadCount) { WorkQueue() }
fun dispatch(coroutine: Coroutine) {
val queue = queues[currentThreadIndex()]
queue.add(coroutine)
}
fun runWorker(threadIndex: Int) {
while (true) {
val task = queues[threadIndex].poll()
task?.run() // 执行协程代码
}
}
}
- 无系统调用:调度完全在用户空间完成
- 批量切换:多个协程在同一线程运行
- 协作式调度:在挂起点切换,缓存友好
3. 无内核切换的挂起/恢复
kotlin
suspend fun networkRequest(url: String): String {
// 1. 设置挂起状态(不离开用户空间)
val callback = suspendCoroutine { continuation ->
// 2. 注册回调给I/O框架
submitIoRequest(url) { result ->
// 3. I/O完成后恢复
continuation.resume(result)
}
}
return callback
}
- 挂起时:只保存少量寄存器到堆,不切换内核态
- 恢复时:直接跳转到暂停点,无调度器介入
性能对比数据
创建操作对比(10000 次)
机制 | 时间 | 内存开销 |
---|---|---|
线程 | 1200-5000 ms | 1.2 GB |
协程 | 10-50 ms | 2-10 MB |
上下文切换性能(100万次切换)
场景 | 线程时间 | 协程时间 |
---|---|---|
简单切换 | 2000-4000 ms | 200-400 ms |
带缓存污染 | 5000-8000 ms | 300-500 ms |
跨核心切换 | 3000-6000 ms | 不适用(同线程内) |
适用场景建议
使用线程的场景
- CPU 密集型计算(长期占用核心)
- 原生库调用(需要实际 OS 线程)
- 低延迟实时系统(协程调度有不确定性)
- 不使用协程的遗留系统
优先使用协程的场景
- I/O 密集型应用(Web 服务、数据库访问)
- 高并发连接处理(如聊天服务器)
- 异步用户界面
- 需要高扩展性的后端系统
结论
线程创建和切换的高成本主要体现在:
- 内存开销大:每个线程 MB 级固定开销
- 内核交互多:每次切换需用户态/内核态切换
- 缓存效率低:核心迁移导致缓存失效
- 资源限制严:系统级线程数上限低
协程通过以下创新解决这些问题:
- 栈复用技术:内存开销降低 100-1000 倍
- 用户态调度:避免内核交互,减少 90% 切换时间
- 协作式挂起:在 I/O 点切换,保持缓存热度
- 结构化并发:生命周期管理更安全
在现代高并发场景下,协程提供比线程高 10-100 倍的并发能力,同时降低资源消耗和延迟,代表了并发编程的未来发展方向。