kotlin协程详解

Kotlin 协程技术文档


简介

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


协程基本概念

什么是协程

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

协程与线程的对比

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

协程构建块

CoroutineScope

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

示例:

scss 复制代码
// 自定义 CoroutineScope
val myScope = CoroutineScope(Dispatchers.Main + Job())

启动协程:launch 与 async

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

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

    kotlin 复制代码
    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 不受限于特定线程,立即在当前线程中执行,通常用于测试或特殊场景。

示例:

scss 复制代码
// 在 IO 线程中执行网络请求
CoroutineScope(Dispatchers.IO).launch {
    val data = fetchDataFromNetwork()
    // 切换回主线程更新 UI
    withContext(Dispatchers.Main) {
        updateUI(data)
    }
}

自定义调度器

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

ini 复制代码
val myExecutor = Executors.newFixedThreadPool(4)
val myDispatcher = myExecutor.asCoroutineDispatcher()

withContext() 的使用

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

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


基本用法

kotlin 复制代码
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

kotlin 复制代码
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 密集型计算(如排序、加密、数据处理)。

kotlin 复制代码
suspend fun heavyComputation() {
    withContext(Dispatchers.Default) {
        val result = (1..1_000_000).sum()
        println("Computation result: $result")
    }
}

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


3. 读取本地文件(IO 线程)
kotlin 复制代码
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 默认情况下,如果子协程发生异常,会取消整个作用域内所有协程。

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

    ini 复制代码
    supervisorScope {
        val job1 = launch {
            // 子任务 1
        }
        val job2 = launch {
            // 子任务 2
        }
    }

协程取消与超时控制

取消协程

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

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

示例:

kotlin 复制代码
val job = myScope.launch {
    repeat(1000) { i ->
        if (!isActive) return@launch // 检查协程是否被取消
        println("Processing $i")
        delay(100)
    }
}
// 取消协程
job.cancel()

withTimeout 与 withTimeoutOrNull

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

    scss 复制代码
    try {
        withTimeout(3000) {
            // 如果 3 秒内没有完成,则异常退出
            performLongRunningTask()
        }
    } catch (e: TimeoutCancellationException) {
        println("任务超时")
    }
  • withTimeoutOrNull 超时返回 null,不会抛异常。

    scss 复制代码
    val result = withTimeoutOrNull(3000) {
        performLongRunningTask()
    }
    if (result == null) {
        println("任务超时")
    }

异常处理

CoroutineExceptionHandler

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

javascript 复制代码
val handler = CoroutineExceptionHandler { _, exception ->
    println("捕获到异常:${exception.message}")
}
myScope.launch(handler) {
    throw RuntimeException("测试异常")
}

try/catch 在协程中的使用

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

kotlin 复制代码
myScope.launch {
    try {
        val result = performRiskOperation()
        println("结果:$result")
    } catch (e: Exception) {
        println("错误:${e.message}")
    }
}

协程通信与同步

Channel

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

示例:

scss 复制代码
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 {} 处理数据
scss 复制代码
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:

scss 复制代码
CoroutineScope(Dispatchers.Main).launch {
    val data = withContext(Dispatchers.IO) { fetchDataFromNetwork() }
    textView.text = data
}

使用 ViewModelScope 和 LifecycleScope

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

    kotlin 复制代码
    class MyViewModel : ViewModel() {
        init {
            viewModelScope.launch {
                // 执行网络请求或数据加载
            }
        }
    }
  • lifecycleScope 适用于 Activity 或 Fragment,自动管理协程生命周期。

    kotlin 复制代码
    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。
相关推荐
Danny_FD4 分钟前
前端BFC详解:从基础到深入的全面解读
前端
临枫38813 分钟前
网页图像优化:现代格式与响应式技巧
前端
咪库咪库咪24 分钟前
构建交互网站
前端
好学人24 分钟前
Kotlin object 关键字详解
kotlin
周星星日记25 分钟前
10.vue3中组件实现原理(上)
前端·vue.js·面试
好学人26 分钟前
Kotlin sealed 关键字介绍
kotlin
小华同学ai26 分钟前
6.4K star!轻松搞定专业领域大模型推理,这个知识增强框架绝了!
前端·github
专业抄代码选手29 分钟前
【VUE】在vue中,Watcher与Dep的关系
前端·面试
Lazy_zheng33 分钟前
从 DOM 监听到 Canvas 绘制:一套完整的水印实现方案
前端·javascript·面试
尘寰ya34 分钟前
前端面试-微前端
前端·面试·职场和发展