Kotlin 协程技术文档


简介

Kotlin 协程是 Kotlin 官方提供的一种用于简化异步编程和并发任务处理的工具。它能够让你用同步的写法实现异步代码,大大简化了回调地狱(callback hell)问题,提高了代码的可读性和维护性。协程在 Android 开发、服务器开发等场景中都有广泛应用。


协程基本概念

什么是协程

协程是一种轻量级的线程,能够在单个线程内并发执行多个任务。它基于挂起(suspending)和恢复(resuming)机制,在任务遇到耗时操作(如 IO、网络请求)时挂起执行,不阻塞线程,待条件满足后恢复执行。

协程与线程的对比

  • 轻量性:协程比线程更加轻量,一个应用可以同时启动成千上万个协程,而线程数量通常受限于系统资源。
  • 调度模型:协程由调度器(Dispatchers)管理,可以在多个线程间灵活调度,而线程调度依赖于操作系统。
  • 切换成本:协程的上下文切换成本远低于线程切换,性能开销较小。
  • 编程模型:协程可以用顺序化的代码编写异步逻辑,避免回调嵌套,使代码更直观。

协程构建块

CoroutineScope

  • 定义CoroutineScope 表示一个协程作用域,它限定了协程的生命周期。所有在该作用域内启动的协程都遵循同一个生命周期,便于集中管理。
  • 创建:可以通过 GlobalScope(不推荐使用)、CoroutineScope(Job()) 或 Android 的 viewModelScopelifecycleScope 来创建作用域。

示例:

1
2
// 自定义 CoroutineScope
val myScope = CoroutineScope(Dispatchers.Main + Job())

启动协程:launch 与 async

  • launch
    用于启动一个不需要返回结果的协程,返回一个 Job 对象。

    1
    2
    3
    4
    5
    myScope.launch {
    // 在协程中执行耗时操作
    delay(1000)
    println("Hello from launch!")
    }
  • async
    用于启动一个协程并返回一个 Deferred 对象,表示未来可能返回的结果。常用于并行计算。

    1
    2
    3
    4
    5
    6
    7
    val deferredResult = myScope.async {
    // 执行计算并返回结果
    delay(500)
    return@async 42
    }
    // 获取结果,注意:await() 是挂起函数
    val result = deferredResult.await()

Job 与 Deferred

  • Job:表示一个协程任务,主要用于管理协程的生命周期(如取消协程)。
  • Deferred:继承自 Job,表示一个带有返回值的任务,使用 await() 方法可以挂起协程等待结果。

调度器 (Dispatchers)

调度器决定协程在哪个线程或线程池上运行。常用的调度器包括:

常见调度器

  • Dispatchers.Main
    用于更新 UI 的主线程,适用于 Android 中与 UI 相关的操作。
  • Dispatchers.IO
    用于执行 IO 密集型任务,如网络请求、磁盘操作等。
  • Dispatchers.Default
    用于执行 CPU 密集型任务,如复杂计算。
  • Dispatchers.Unconfined
    不受限于特定线程,立即在当前线程中执行,通常用于测试或特殊场景。

示例:

1
2
3
4
5
6
7
8
// 在 IO 线程中执行网络请求
CoroutineScope(Dispatchers.IO).launch {
val data = fetchDataFromNetwork()
// 切换回主线程更新 UI
withContext(Dispatchers.Main) {
updateUI(data)
}
}

自定义调度器

可以创建自定义线程池,并用 asCoroutineDispatcher() 方法转换为协程调度器:

1
2
val myExecutor = Executors.newFixedThreadPool(4)
val myDispatcher = myExecutor.asCoroutineDispatcher()

withContext() 的使用

withContext() 是 Kotlin 协程中的 挂起函数,用于 在协程中切换执行的调度器。它不会创建新的协程,而是挂起当前协程并在指定的调度器上执行代码块,执行完成后恢复到原来的调度器。

withContext() 会挂起外部协程,直到内部代码执行完毕,才继续执行外部协程的后续代码,但不会阻塞线程


基本用法

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.*

fun main() = runBlocking {
println("Start: ${Thread.currentThread().name}")

withContext(Dispatchers.IO) {
println("Switch to IO: ${Thread.currentThread().name}")
delay(1000) // 模拟耗时操作
}

println("Back to main: ${Thread.currentThread().name}")
}

执行过程

  1. runBlocking主线程 中运行。
  2. withContext(Dispatchers.IO) 切换到 IO 线程池 并执行代码块。
  3. delay(1000) 让协程挂起 1 秒,但不会阻塞线程。
  4. withContext 执行完毕,恢复到 原来的调度器(主线程)

常见用途

1. 在 Dispatchers.Main 执行 UI 更新

适用于 Android 开发,用于 在后台线程执行任务,并回到主线程更新 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
val data = withContext(Dispatchers.IO) { fetchData() }
textView.text = data // 回到主线程更新 UI
}
}

private suspend fun fetchData(): String {
delay(2000) // 模拟网络请求
return "Data from server"
}
}

🔹 fetchData()IO 线程执行,获取数据后 withContext 自动 回到主线程 更新 textView


2. 计算密集型任务 (Dispatchers.Default)

适用于 CPU 密集型计算(如排序、加密、数据处理)。

1
2
3
4
5
6
suspend fun heavyComputation() {
withContext(Dispatchers.Default) {
val result = (1..1_000_000).sum()
println("Computation result: $result")
}
}

🔹 计算任务会在 默认线程池Default)运行,不会阻塞 UI。


3. 读取本地文件(IO 线程)

1
2
3
4
5
suspend fun readFile(): String {
return withContext(Dispatchers.IO) {
File("data.txt").readText()
}
}

🔹 避免主线程阻塞,文件读取在 IO 线程 完成。


⚠️ 注意事项

  1. withContext() 不会创建新的协程,只是切换执行环境。
  2. withContext() 适合执行一个需要返回结果的任务,如果要 同时运行多个任务,用 launchasync
  3. 不要在 Dispatchers.Main 中使用 withContext(Dispatchers.Main),会导致 额外的调度开销

🚀 总结

  • withContext() 用于 切换线程并执行代码,执行完自动回到原来的线程。
  • 适用于 IO 操作、计算任务、UI 更新等场景。
  • 不会创建新协程,而是 挂起当前协程 并切换调度器。

💡 适用场景: ✅ 网络请求Dispatchers.IO
数据库操作Dispatchers.IO
计算密集型任务Dispatchers.Default
UI 更新Dispatchers.Main

结构化并发

作用域与协程层级关系

结构化并发(Structured Concurrency)要求协程在层次化的作用域内启动,确保父协程管理子协程的生命周期,避免出现未管理的孤儿协程。这样可以保证资源的及时释放与错误的统一处理。

CoroutineScope 与 supervisorScope

  • CoroutineScope
    默认情况下,如果子协程发生异常,会取消整个作用域内所有协程。

  • supervisorScope
    supervisorScope 内,一个子协程的异常不会传播到其他子协程,可以实现部分任务失败而不影响整体流程。

    1
    2
    3
    4
    5
    6
    7
    8
    supervisorScope {
    val job1 = launch {
    // 子任务 1
    }
    val job2 = launch {
    // 子任务 2
    }
    }

协程取消与超时控制

取消协程

协程取消是一种协作机制,被取消的协程需要在合适的地方检测取消状态。常用方法包括:

  • 调用 job.cancel():取消当前 Job 以及其所有子协程。
  • 挂起点检测:例如 delay()withContext() 都会检查取消状态。

示例:

1
2
3
4
5
6
7
8
9
val job = myScope.launch {
repeat(1000) { i ->
if (!isActive) return@launch // 检查协程是否被取消
println("Processing $i")
delay(100)
}
}
// 取消协程
job.cancel()

withTimeout 与 withTimeoutOrNull

  • withTimeout
    规定一段时间内未完成任务,则抛出 TimeoutCancellationException

    1
    2
    3
    4
    5
    6
    7
    8
    try {
    withTimeout(3000) {
    // 如果 3 秒内没有完成,则异常退出
    performLongRunningTask()
    }
    } catch (e: TimeoutCancellationException) {
    println("任务超时")
    }
  • withTimeoutOrNull
    超时返回 null,不会抛异常。

    1
    2
    3
    4
    5
    6
    val result = withTimeoutOrNull(3000) {
    performLongRunningTask()
    }
    if (result == null) {
    println("任务超时")
    }

异常处理

CoroutineExceptionHandler

用于全局捕获协程未处理的异常。通过在协程上下文中添加 CoroutineExceptionHandler,可以统一处理异常。

1
2
3
4
5
6
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获到异常:${exception.message}")
}
myScope.launch(handler) {
throw RuntimeException("测试异常")
}

try/catch 在协程中的使用

在挂起函数内部,同步代码中的 try/catch 同样适用,用于捕获特定的异常。

1
2
3
4
5
6
7
8
myScope.launch {
try {
val result = performRiskOperation()
println("结果:$result")
} catch (e: Exception) {
println("错误:${e.message}")
}
}

协程通信与同步

Channel

Channel 提供了一种在协程之间进行通信的方式,类似于阻塞队列。通过 Channel,可以实现生产者-消费者模型。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import kotlinx.coroutines.channels.Channel

val channel = Channel<Int>()
// 生产者协程
launch {
for (i in 1..5) {
channel.send(i)
}
channel.close() // 关闭 Channel
}
// 消费者协程
launch {
for (number in channel) {
println("接收数据: $number")
}
}

Flow

Flow 是 Kotlin 协程中的响应式流,用于处理异步数据流和背压控制。

  • 定义 Flow:使用 flow {} 构建数据流
  • 收集数据:使用 collect {} 处理数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import kotlinx.coroutines.flow.*

fun simpleFlow(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i) // 发射数据
}
}

launch {
simpleFlow().collect { value ->
println("收到:$value")
}
}

与 Android 集成

在 Android 开发中,协程可以很好地解决异步任务和 UI 更新问题。常见的集成方式包括:

在 UI 线程中启动协程

利用 Dispatchers.Main 在主线程中更新 UI:

1
2
3
4
CoroutineScope(Dispatchers.Main).launch {
val data = withContext(Dispatchers.IO) { fetchDataFromNetwork() }
textView.text = data
}

使用 ViewModelScope 和 LifecycleScope

  • viewModelScope
    适用于 ViewModel 中启动协程,与 ViewModel 生命周期绑定,避免内存泄漏。

    1
    2
    3
    4
    5
    6
    7
    class MyViewModel : ViewModel() {
    init {
    viewModelScope.launch {
    // 执行网络请求或数据加载
    }
    }
    }
  • lifecycleScope
    适用于 Activity 或 Fragment,自动管理协程生命周期。

    1
    2
    3
    4
    5
    6
    7
    8
    class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launch {
    // 执行与 UI 相关的协程操作
    }
    }
    }

最佳实践与常见问题

最佳实践

  1. 避免使用 GlobalScope
    全局作用域容易导致协程泄漏,建议使用结构化并发的 CoroutineScope。
  2. 选择合适的调度器
    根据任务类型(IO、CPU、UI)选择对应的调度器。
  3. 合理使用超时与取消
    对长时间等待的任务使用 withTimeout/withTimeoutOrNull,确保协程及时取消。
  4. 异常捕获
    在关键业务逻辑中使用 try/catch,同时配置 CoroutineExceptionHandler 捕获全局异常。
  5. 整洁代码
    将协程相关代码放入专门的 Repository 或 UseCase 层,分离 UI 层逻辑。

常见问题

  • 协程泄漏
    未取消的协程会导致内存泄漏。确保在合适时机调用 cancel() 或使用绑定生命周期的 Scope。
  • 异常未捕获
    子协程异常可能不会传递给父协程,使用 supervisorScope 或者合适的异常处理器来保证异常处理一致性。
  • 线程切换问题
    协程中的上下文切换需要显式指定 withContext,否则可能导致 UI 阻塞或后台线程操作 UI。