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。
相关推荐
巴巴_羊41 分钟前
yarn npm pnpm
前端·npm·node.js
chéng ௹2 小时前
vue2 上传pdf,拖拽盖章,下载图片
前端·css·pdf
嗯.~2 小时前
【无标题】如何在sheel中运行Spark
前端·javascript·c#
A_aspectJ5 小时前
【Bootstrap V4系列】学习入门教程之 组件-输入组(Input group)
前端·css·学习·bootstrap·html
兆。5 小时前
电子商城后台管理平台-Flask Vue项目开发
前端·vue.js·后端·python·flask
互联网搬砖老肖5 小时前
Web 架构之负载均衡全解析
前端·架构·负载均衡
sunbyte6 小时前
Tailwind CSS v4 主题化实践入门(自定义 Theme + 主题模式切换)✨
前端·javascript·css·tailwindcss
ghie90906 小时前
Kotlin中Lambda表达式和匿名函数的区别
java·算法·kotlin
湛海不过深蓝7 小时前
【css】css统一设置变量
前端·css
程序员的世界你不懂7 小时前
tomcat6性能优化
前端·性能优化·firefox