Kotlin 协程详解:原理、使用及基础实践

875 阅读8分钟

Kotlin 协程是 JetBrains 为简化异步编程而引入的轻量级线程工具。协程可以在不阻塞线程的情况下执行异步代码,使代码更加简洁和易于理解。本文将深入探讨 Kotlin 协程的工作原理、核心概念、使用方法及最佳实践。

1. 协程概述

1.1 什么是协程?

协程是一种可以在一个线程内挂起和恢复的轻量级并发设计。与传统的多线程并发不同,协程不需要创建新线程,可以在单个线程内实现异步操作,从而显著减少资源消耗和上下文切换的开销。

开发者需要明白,协程是运行于线程上的,一个线程可以运行多个(可以是几千上万个)协程。线程的调度行为是由 OS 来操纵的,而协程的调度行为是可以由开发者来指定并由编译器来实现的。当协程 A 调用 delay(1000L) 函数来指定延迟1秒后再运行时,协程 A 所在的线程只是会转而去执行协程 B,等到1秒后再把协程 A 加入到可调度队列里。所以说,线程并不会因为协程的延时而阻塞,这样可以极大地提高线程的并发灵活度

1.2 协程的优点

  • 简洁性:协程使异步代码看起来像同步代码,减少了回调地狱的问题。
  • 轻量级:协程比线程更加轻量,可以在同一个线程内运行成千上万的协程。
  • 易于控制:协程提供了更好的控制,如取消协程、超时处理等。

2. 协程的基本概念

2.1 启动协程

Kotlin 提供了多种启动协程的方式,主要包括 launchasync

launchasync
  • launch:启动一个新的协程,返回一个 Job 对象,不返回结果。
  • async:启动一个新的协程,返回一个 Deferred 对象,可以用 await 获取结果。
kotlin
复制代码
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 使用 launch 启动协程
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000L)  // 主协程延迟2秒以保证JVM存活
}

// 使用 async 启动协程并返回结果
fun main() = runBlocking {
    val deferred = async {
        delay(1000L)
        "World!"
    }
    println("Hello, ${deferred.await()}")
}

可以将 async 的 start 参数设置为 CoroutineStart.lazy 使其变为懒加载模式。在这种模式下,只有在主动调用 Deferred 的 await() 或者 start() 方法时才会启动协程。

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    //sampleStart
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // some computation
        one.start() // start the first one
        two.start() // start the second one
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
    //sampleEnd    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

2.2 runBlocking

runBlocking 用于在当前线程启动一个新的协程,并阻塞当前线程直到协程执行完毕。通常用于测试或示例代码中。

kotlin
复制代码
fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Kotlin Coroutines!")
    }
    println("Hello,")
}

2.3 Job 和 Deferred

  • Job:一个可取消的计算任务,表示协程的生命周期。
  • Deferred:一个带返回值的 Job,通过 await 方法获取结果。
kotlin
复制代码
fun main() = runBlocking {
    val job = launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 等待job完成
}

fun main() = runBlocking {
    val deferred = async {
        delay(1000L)
        "World!"
    }
    println("Hello, ${deferred.await()}")
}

2.4协程的生命周期

协程的生命周期由其所在的 CoroutineScope 控制,并与其所启动的作用域的生命周期绑定。协程的生命周期通常包括以下几个阶段:

协程的状态
  1. NEW:协程已创建但尚未启动。
  2. ACTIVE:协程正在运行或挂起等待结果。
  3. CANCELLING:协程正在被取消。
  4. CANCELLED:协程已取消,不再活动。
  5. COMPLETED:协程已执行完毕。

3. 挂起函数

挂起函数是一种特殊的函数,可以在协程中被挂起和恢复。使用 suspend 关键字定义。

kotlin
复制代码
suspend fun mySuspendFunction() {
    delay(1000L)
    println("Hello from suspend function")
}

4. 协程上下文和调度器

协程上下文和调度器用于控制协程运行的线程。

4.1 Dispatchers

  • Dispatchers.Main:在主线程上运行,用于更新 UI。
  • Dispatchers.IO:用于执行 I/O 操作,如网络请求、文件读写。
  • Dispatchers.Default:用于执行 CPU 密集型任务。
  • Dispatchers.Unconfined:不限制协程运行的线程。
kotlin
复制代码
fun main() = runBlocking {
    launch(Dispatchers.Main) {
        println("Running on the main thread")
    }
    launch(Dispatchers.IO) {
        println("Running on the IO thread")
    }
    launch(Dispatchers.Default) {
        println("Running on the Default thread")
    }
    launch(Dispatchers.Unconfined) {
        println("Running on the Unconfined thread")
    }
}

4.2 CoroutineScope 和 CoroutineContext

CoroutineScope 定义了一个协程的范围,CoroutineContext 则包含协程的各种上下文元素,如 Job、调度器等。

协程必须在一个作用域内启动,以确保其生命周期得到正确管理。CoroutineScope 是管理协程生命周期的核心接口,所有协程的启动都必须关联到某个作用域。

  • GlobalScope:全局协程作用域,其生命周期与整个应用程序一致。由于其生命周期较长,使用时需谨慎,避免内存泄漏。
  • CoroutineScope:可以手动创建的局部协程作用域,适用于与特定生命周期绑定的协程管理,如在 ActivityFragment 中启动的协程。
kotlin
复制代码
class MyActivity : AppCompatActivity(), CoroutineScope {
    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
        launch {
            // 协程体
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

协程作用域的创建方式多种多样,可以根据需求选择合适的方式来控制协程的生命周期。以下是几种常见的创建方式:

4.2.1. 使用 GlobalScope

GlobalScope 是一个全局的协程作用域,生命周期与整个应用程序相同。它通常用于启动具有整个应用程序生命周期的长时间运行任务。

kotlin
复制代码
GlobalScope.launch {
    // 在全局作用域中启动一个协程
}

注意GlobalScope 是不推荐频繁使用的,因为它与应用程序的生命周期绑定,容易导致内存泄漏或无法正确取消协程。

4.2.2. 使用 runBlocking

runBlocking 是一种特殊的协程构建器,它会启动一个新的协程并阻塞当前线程,直到协程执行完毕。它通常用于测试或主函数中。

kotlin
复制代码
fun main() = runBlocking {
    // 在阻塞的作用域中启动协程
    launch {
        println("Hello, Coroutine!")
    }
}

注意runBlocking 会阻塞调用它的线程,所以不应在 UI 线程中使用。

4.2.3. 使用 CoroutineScope

CoroutineScope 是最常见的创建协程作用域的方式。你可以在类中定义一个 CoroutineScope,并根据类的生命周期管理协程。

kotlin
复制代码
class MyActivity : AppCompatActivity() {
    private val scope = CoroutineScope(Dispatchers.Main)

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()  // 取消该作用域中的所有协程
    }

    fun someFunction() {
        scope.launch {
            // 启动协程
        }
    }
}
4.2.4. 使用 MainScope

MainScope 是 Android 中专门为 UI 线程提供的一个 CoroutineScope,其默认调度器为 Dispatchers.Main。这通常用于 Android 开发中 ActivityFragment 的生命周期管理。

kotlin
复制代码
class MyActivity : AppCompatActivity() {
    private val scope = MainScope()

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()
    }
}
4.2.5. 使用 ViewModelScope

ViewModelScope 是 Android Jetpack 提供的作用域,专门用于 ViewModel 中的协程管理,协程的生命周期与 ViewModel 相同。当 ViewModel 清除时,该作用域内的所有协程都会被取消。

kotlin
复制代码
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            // 在 ViewModel 范围内启动协程
        }
    }
}
4.2.6. 使用 lifecycleScope

lifecycleScopeLifecycleOwner(如 ActivityFragment)提供的作用域,协程的生命周期与 LifecycleOwner 的生命周期绑定。可用于启动与 UI 生命周期关联的任务。

kotlin
复制代码
class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        lifecycleScope.launch {
            // 在 Lifecycle 范围内启动协程
        }
    }
}
4.2.7. 自定义 CoroutineScope

你可以通过手动创建一个 CoroutineScope 并结合 JobDispatchers 来实现更灵活的协程管理。

kotlin
复制代码
val customScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

fun someFunction() {
    customScope.launch {
        // 自定义作用域中的协程
    }
}

// 在需要的时候取消协程
customScope.cancel()
4.2.8. 使用 supervisorScope

supervisorScope 是一种特殊的作用域构建器,内部的子协程如果发生异常,异常不会传递给其他协程。适用于当一个协程失败时不影响其他协程的场景。

kotlin
复制代码
supervisorScope {
    launch {
        // 这里的异常不会影响其他协程
    }
}

5. 协程的取消与超时

5.1 取消协程

使用 cancel 方法取消协程。

kotlin
复制代码
fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("Job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并等待其完成
    println("main: Now I can quit.")
}

5.2 超时处理

使用 withTimeout 方法设置协程超时时间。

kotlin
复制代码
fun main() = runBlocking {
    try {
        withTimeout(1300L) {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        }
    } catch (e: TimeoutCancellationException) {
        println("Timeout!")
    }
}

6. 实际应用示例

以下是一个使用协程进行网络请求的示例:

kotlin
复制代码
import kotlinx.coroutines.*
import java.net.URL

suspend fun fetchUrl(url: String): String {
    return withContext(Dispatchers.IO) {
        URL(url).readText()
    }
}

fun main() = runBlocking {
    val result = fetchUrl("https://www.example.com")
    println(result)
}

7. 最佳实践

7.1 使用适当的调度器

根据任务类型选择合适的调度器,例如,I/O 操作使用 Dispatchers.IO,CPU 密集型任务使用 Dispatchers.Default

kotlin
复制代码
fun main() = runBlocking {
    launch(Dispatchers.IO) {
        // 执行I/O操作
    }
    launch(Dispatchers.Default) {
        // 执行CPU密集型任务
    }
}

7.2 避免全局作用域

尽量避免使用 GlobalScope.launch,可以使用 CoroutineScopeViewModelScope 等。

kotlin
复制代码
class MyViewModel : ViewModel() {
    val viewModelScope = ViewModelScope(Dispatchers.Main)

    init {
        viewModelScope.launch {
            // 协程体
        }
    }
}

7.3 处理异常

使用 try-catch 或者 CoroutineExceptionHandler 来处理协程中的异常。

kotlin
复制代码
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }

    val job = GlobalScope.launch(handler) {
        throw AssertionError("My Custom Exception")
    }

    job.join()
}

supervisorScope 是一种特殊的协程作用域,它可以避免子协程的异常传播,从而确保一个协程的失败不会影响到其他协程。

kotlin
复制代码
fun main() = runBlocking {
    supervisorScope {
        val job1 = launch {
            throw IOException("Job1 failed")
        }
        val job2 = launch {
            println("Job2 completed")
        }
        job1.join()
        job2.join()
    }
}

在这个例子中,即使 job1 抛出异常,job2 依然会继续执行并完成任务。

7.4 结构化并发

使用结构化并发,确保协程的生命周期受控,避免内存泄漏和资源泄露。

kotlin
复制代码
fun main() = runBlocking {
    coroutineScope {
        launch {
            delay(1000L)
            println("Task from runBlocking")
        }

        launch {
            delay(500L)
            println("Task from nested launch")
        }
    }

    println("Coroutine scope is over")
}

协程在处理异步任务方面表现卓越,例如网络请求和文件读写等 I/O 操作。

kotlin
复制代码
fun main() = runBlocking {
    val deferred = async(Dispatchers.IO) {
        fetchUserData()  // 假设是一个挂起函数
    }
    println(deferred.await())
}

通过使用 async 构建器,协程能够在多个 CPU 核心上并行执行计算任务。

kotlin
复制代码
fun main() = runBlocking {
    val result1 = async { compute1() }
    val result2 = async { compute2() }
    println(result1.await() + result2.await())
}

8. 协程的高级概念

8.1 协程的挂起和恢复

协程的挂起和恢复是通过 Kotlin 编译器在编译时生成状态机来实现的。每次调用挂起函数时,协程都会保存当前状态并挂起执行,等到恢复时继续执行。

8.2 协程的线程切换

协程可以通过调度器在不同的线程之间切换。通过使用 withContext 函数,协程可以在不同的调度器之间切换执行。

kotlin
复制代码
suspend fun compute() = withContext(Dispatchers.Default) {
    // 在默认调度器上执行计算任务
}

suspend fun fetchData() = withContext(Dispatchers.IO) {
    // 在IO调度器上执行网络请求
}

8.3 协程的取消原理

协程的取消是协作式的。协程会定期检查自身是否被取消,通过 isActive 属性或 yield 函数来响应取消请求。

kotlin
复制代码
fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            if (!isActive) return@launch
            println("Job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L)
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

9. 协程与线程的比较

协程虽然在很多方面与线程类似,但它们在实现方式和应用场景上存在显著差异。

9.1 协程与线程的异同
特性协程线程
重量轻量级,数千个协程消耗少量内存重量级,每个线程消耗较多内存
调度协程调度器管理,可以在不同线程间切换操作系统调度,较为固定
上下文切换非阻塞,挂起不会阻塞线程阻塞,线程切换成本高
生命周期CoroutineScope 管理由 JVM 或操作系统管理
资源开销低,协程复用线程资源,消耗极少的资源高,需要分配独立的线程资源
9.2 协程的优越性

由于协程是轻量级的,因此它们适合处理大量并发任务而不会产生过多的内存和线程切换开销。此外,协程的挂起和恢复机制使得它们能够更高效地利用资源。

10. 结论

Kotlin 协程通过其轻量级、易用和高效的特点,为异步编程提供了一种全新的方式。它们使得编写和维护异步代码变得更加简单和直观。通过掌握协程的基本概念、使用方法和一些高级特性,可以更好地应对复杂的异步编程需求。

希望这篇文章能帮助你深入理解 Kotlin 协程,并在实际项目中更好地应用它们。如果你有任何问题或建议,欢迎交流和讨论。