MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin协程基础入门

2021-09-097.7k 阅读

Kotlin 协程是什么

在 Kotlin 编程中,协程是一种轻量级的异步编程模型。它允许我们以一种更简洁、更直观的方式处理异步任务,就像编写同步代码一样,却能在后台线程执行任务,避免阻塞主线程,提高程序的响应性和性能。

从本质上来说,协程是一种基于线程的更细粒度的执行单元。传统的线程对于系统资源的消耗较大,而协程在相同的资源下可以创建更多的执行单元。它通过挂起和恢复机制来实现异步操作,当一个协程遇到挂起函数(比如等待网络请求返回、读取文件等需要耗时的操作)时,它会暂停自身的执行,释放线程资源给其他任务使用,当挂起的条件满足(比如网络请求有了响应、文件读取完成),协程再从暂停的地方恢复执行。

为什么要使用 Kotlin 协程

  1. 简化异步代码:在没有协程之前,处理异步任务通常需要使用回调函数。多层回调嵌套会导致代码可读性变差,形成所谓的 “回调地狱”。而协程允许我们以顺序执行的方式编写异步代码,让代码结构更清晰。 例如,假设我们有一个需要依次进行网络请求和数据库操作的任务,使用回调的方式可能如下:
networkRequest { response ->
    databaseOperation(response) { result ->
        // 处理最终结果
    }
}

而使用协程可以写成:

val response = networkRequest()
val result = databaseOperation(response)
// 处理最终结果
  1. 提高代码的响应性:在 Android 开发等场景中,主线程负责处理用户界面的更新等操作。如果在主线程执行耗时任务,会导致界面卡顿。协程可以将耗时任务放在后台线程执行,主线程继续处理其他用户交互,提高应用的响应性。
  2. 更好的资源管理:协程是轻量级的,创建和销毁的开销较小。相比传统线程,在需要大量异步任务并发执行的场景下,协程能更有效地利用系统资源。

协程的基本构建块

  1. CoroutineScope(协程作用域)
    • 定义:CoroutineScope 是协程的执行上下文,它决定了协程在何处执行以及何时结束。每个协程都必须在一个 CoroutineScope 中启动。
    • 作用:它提供了对协程生命周期的控制。当 CoroutineScope 被取消时,所有在该作用域内启动的协程都会被取消。例如,在 Android 中,我们可以将协程绑定到 Activity 或 Fragment 的生命周期上,当 Activity 或 Fragment 销毁时,相关的协程也会被取消,避免内存泄漏。
    • 示例
import kotlinx.coroutines.*

// 创建一个 CoroutineScope
val scope = CoroutineScope(Job() + Dispatchers.Default)

fun main() {
    scope.launch {
        // 协程代码
        println("Coroutine is running")
    }
    runBlocking {
        delay(1000)
    }
}

在这个例子中,scope 是一个自定义的 CoroutineScope,通过 Job()Dispatchers.Default 创建。Job() 用于管理协程的生命周期,Dispatchers.Default 表示协程将在默认的后台线程池中执行。scope.launch 启动了一个新的协程。

  1. launch
    • 定义launch 是 CoroutineScope 的一个扩展函数,用于启动一个新的协程。它返回一个 Job 对象,通过这个 Job 对象可以对协程进行控制,比如取消协程。
    • 作用:启动一个异步任务,该任务在 CoroutineScope 定义的上下文中执行。
    • 示例
import kotlinx.coroutines.*

fun main() {
    val scope = CoroutineScope(Job() + Dispatchers.Default)
    val job = scope.launch {
        for (i in 1..5) {
            println("Count: $i")
            delay(1000)
        }
    }
    runBlocking {
        delay(3000)
        job.cancel()
    }
}

这里 scope.launch 启动了一个协程,协程会每隔 1 秒打印一次 CountrunBlocking 函数暂停主线程,3 秒后通过 job.cancel() 取消协程。

  1. async
    • 定义async 也是 CoroutineScope 的扩展函数,它和 launch 类似,用于启动一个异步任务,但 async 返回一个 Deferred 对象。Deferred 是一个可延迟获取结果的对象,类似于 Java 中的 Future。
    • 作用:当我们需要异步执行一个任务并获取其返回结果时,就可以使用 async
    • 示例
import kotlinx.coroutines.*

fun main() = runBlocking {
    val scope = CoroutineScope(Job() + Dispatchers.Default)
    val deferred = scope.async {
        // 模拟耗时操作
        delay(2000)
        42
    }
    println("Waiting for result")
    val result = deferred.await()
    println("Result is: $result")
}

在这个例子中,scope.async 启动了一个异步任务,任务延迟 2 秒后返回 42。deferred.await() 会暂停当前协程,直到 async 启动的协程执行完毕并返回结果。

  1. runBlocking
    • 定义runBlocking 是一个顶层函数,它创建一个新的 CoroutineScope,并在其中启动一个协程,同时阻塞当前线程,直到内部的协程执行完毕。
    • 作用:通常用于测试或者在一些需要将异步操作转换为同步操作的场景。比如在 main 函数中,因为 main 函数默认是同步执行的,我们可以使用 runBlocking 来运行协程代码。
    • 示例
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        for (i in 1..3) {
            println("Inner Coroutine: $i")
            delay(1000)
        }
    }
    for (i in 1..3) {
        println("Outer: $i")
        delay(1500)
    }
}

在这个例子中,runBlocking 内部启动了一个协程,同时外部也有一个循环。runBlocking 会阻塞 main 函数所在的线程,直到内部协程和外部循环都执行完毕。

协程调度器(Dispatchers)

  1. Dispatchers.Default
    • 定义Dispatchers.Default 表示使用默认的后台线程池来执行协程。这个线程池适用于 CPU 密集型任务,比如复杂的计算。
    • 作用:将协程分配到一个后台线程池中执行,避免阻塞主线程。它的线程数量会根据系统的 CPU 核心数动态调整,以充分利用系统资源。
    • 示例
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        // 模拟 CPU 密集型任务
        var sum = 0
        for (i in 1..1000000) {
            sum += i
        }
        println("Sum is: $sum")
    }
}

这里 launch(Dispatchers.Default) 将协程分配到默认的后台线程池执行复杂的计算任务。

  1. Dispatchers.IO
    • 定义Dispatchers.IO 用于执行 I/O 相关的任务,比如文件读写、网络请求等。它使用一个专门的线程池来处理这些 I/O 操作。
    • 作用:I/O 操作通常是耗时的且不占用大量 CPU 资源,Dispatchers.IO 线程池可以有效地管理这些任务,提高 I/O 操作的效率。
    • 示例
import kotlinx.coroutines.*
import java.io.File

fun main() = runBlocking {
    launch(Dispatchers.IO) {
        val file = File("test.txt")
        file.writeText("Hello, Kotlin Coroutines!")
        val content = file.readText()
        println("File content: $content")
    }
}

在这个例子中,文件的读写操作在 Dispatchers.IO 线程池中执行,避免阻塞主线程。

  1. Dispatchers.Main
    • 定义:在 Android 开发中,Dispatchers.Main 用于在主线程(UI 线程)上执行协程。只有在主线程才能更新 UI 元素。
    • 作用:当我们需要在异步任务完成后更新 UI 时,就可以使用 Dispatchers.Main 将协程切换到主线程执行。
    • 示例
// 假设这是一个 Android 中的 Activity
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.textView)
        CoroutineScope(Job() + Dispatchers.IO).launch {
            // 模拟网络请求
            delay(2000)
            withContext(Dispatchers.Main) {
                textView.text = "Network request completed"
            }
        }
    }
}

在这个 Android 示例中,先在 Dispatchers.IO 中模拟网络请求,完成后通过 withContext(Dispatchers.Main) 切换到主线程更新 TextView 的内容。

挂起函数

  1. 定义:挂起函数是 Kotlin 协程中一种特殊的函数,它可以暂停协程的执行,并且只有在满足一定条件时才会恢复执行。挂起函数必须在协程或者其他挂起函数内部调用。
  2. 作用:挂起函数通常用于处理异步操作,比如网络请求、文件读取等。通过挂起函数,我们可以以同步的方式编写异步代码,提高代码的可读性。
  3. 示例
import kotlinx.coroutines.*

suspend fun fetchData(): String {
    delay(2000)
    return "Data fetched"
}

fun main() = runBlocking {
    val data = fetchData()
    println(data)
}

在这个例子中,fetchData 是一个挂起函数,它内部使用 delay 模拟了一个耗时操作。runBlocking 内部可以直接调用挂起函数 fetchData,代码看起来就像同步执行一样。

协程的生命周期

  1. 创建:当使用 launchasync 启动一个协程时,协程就进入了创建状态。此时协程还没有开始执行。
  2. 就绪:协程创建后,会等待调度器将其分配到一个合适的线程上执行,这个阶段就是就绪状态。
  3. 运行:当协程被调度到一个线程上并开始执行其代码时,就进入了运行状态。
  4. 挂起:如果协程执行到一个挂起函数,它会暂停执行,进入挂起状态。此时线程资源会被释放,用于执行其他任务。
  5. 恢复:当挂起函数的条件满足(比如网络请求返回、文件读取完成),协程会从挂起的地方恢复执行,重新进入运行状态。
  6. 完成:当协程的代码执行完毕或者因为异常终止时,协程就进入了完成状态。

例如:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        println("Coroutine started")
        try {
            delay(2000)
            println("Coroutine resumed after delay")
        } catch (e: Exception) {
            println("Coroutine cancelled: ${e.message}")
        } finally {
            println("Coroutine finished")
        }
    }
    delay(1000)
    job.cancel()
}

在这个例子中,协程启动后打印 Coroutine started,然后进入挂起状态(delay 是挂起函数),1 秒后主线程取消协程,协程捕获到取消异常,打印 Coroutine cancelled 并执行 finally 块中的代码。

协程的并发和并行

  1. 并发:并发是指在同一时间段内,多个任务交替执行。在协程中,多个协程可以在同一个线程上交替执行,通过挂起和恢复机制实现。例如,我们有两个协程 A 和 B,A 执行到一个挂起函数时,线程资源被释放,B 可以在这个线程上开始执行,当 A 的挂起条件满足时,A 又可以恢复执行。
    • 示例
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        for (i in 1..3) {
            println("Coroutine 1: $i")
            delay(1000)
        }
    }
    launch {
        for (i in 1..3) {
            println("Coroutine 2: $i")
            delay(1500)
        }
    }
}

在这个例子中,两个协程在同一个线程上交替执行,根据 delay 的时间不同,输出会交替出现。

  1. 并行:并行是指在同一时刻,多个任务同时执行。在协程中,如果我们将不同的协程分配到不同的线程上执行,就可以实现并行。例如,使用 Dispatchers.Default 创建多个协程,这些协程可能会被分配到不同的线程池中线程上并行执行(具体取决于系统资源和调度器的策略)。
    • 示例
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job1 = GlobalScope.launch(Dispatchers.Default) {
        for (i in 1..3) {
            println("Job 1 on thread: ${Thread.currentThread().name}")
            delay(1000)
        }
    }
    val job2 = GlobalScope.launch(Dispatchers.Default) {
        for (i in 1..3) {
            println("Job 2 on thread: ${Thread.currentThread().name}")
            delay(1500)
        }
    }
    job1.join()
    job2.join()
}

在这个例子中,两个协程 job1job2 都使用 Dispatchers.Default,它们可能会在不同的线程上并行执行,通过打印线程名可以观察到。

协程的异常处理

  1. try - catch 块:和普通的 Kotlin 代码类似,我们可以在协程内部使用 try - catch 块来捕获异常。
    • 示例
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        try {
            throw RuntimeException("An error occurred")
        } catch (e: RuntimeException) {
            println("Caught exception: ${e.message}")
        }
    }
}

在这个例子中,协程内部抛出一个 RuntimeException,通过 try - catch 块捕获并打印异常信息。

  1. CoroutineExceptionHandler:我们还可以通过 CoroutineExceptionHandler 来统一处理协程中的异常。它是一个实现了 CoroutineExceptionHandler 接口的对象,可以传递给 CoroutineScope
    • 示例
import kotlinx.coroutines.*

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    println("Caught by CoroutineExceptionHandler: ${exception.message}")
}

fun main() = runBlocking {
    val scope = CoroutineScope(Job() + Dispatchers.Default + exceptionHandler)
    scope.launch {
        throw RuntimeException("Another error")
    }
}

在这个例子中,CoroutineExceptionHandler 捕获到协程中抛出的 RuntimeException 并打印异常信息。

实战案例:使用 Kotlin 协程进行网络请求和数据处理

假设我们有一个简单的应用场景,需要从网络获取用户数据,然后对数据进行一些处理并显示。我们可以使用 Kotlin 协程结合 Retrofit(一个流行的 Android 网络请求库)来实现。

  1. 添加依赖:在 build.gradle 文件中添加 Retrofit 和 Kotlin 协程相关依赖。
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
  1. 定义 Retrofit 接口
import retrofit2.Response
import retrofit2.http.GET

interface UserApi {
    @GET("users")
    suspend fun getUsers(): Response<List<User>>
}

这里定义了一个 UserApi 接口,getUsers 方法是一个挂起函数,用于获取用户列表。

  1. 创建 Retrofit 实例
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    private const val BASE_URL = "https://example.com/api/"

    private val okHttpClient = OkHttpClient.Builder()
       .build()

    private val retrofit = Retrofit.Builder()
       .baseUrl(BASE_URL)
       .addConverterFactory(GsonConverterFactory.create())
       .client(okHttpClient)
       .build()

    val api: UserApi = retrofit.create(UserApi::class.java)
}
  1. 使用协程进行网络请求和数据处理
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.textView)
        CoroutineScope(Job() + Dispatchers.IO).launch {
            try {
                val response = RetrofitInstance.api.getUsers()
                if (response.isSuccessful) {
                    val users = response.body()
                    val userCount = users?.size?: 0
                    withContext(Dispatchers.Main) {
                        textView.text = "Number of users: $userCount"
                    }
                } else {
                    withContext(Dispatchers.Main) {
                        textView.text = "Network request failed"
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    textView.text = "An error occurred: ${e.message}"
                }
            }
        }
    }
}

在这个 Android 示例中,首先在 Dispatchers.IO 中执行网络请求,成功后切换到 Dispatchers.Main 更新 UI。如果请求失败或发生异常,同样切换到主线程提示用户。

通过以上内容,我们对 Kotlin 协程的基础知识有了较为全面的了解,从基本概念到实际应用,Kotlin 协程为异步编程提供了强大而简洁的方式。无论是在 Android 开发还是其他 Kotlin 应用场景中,熟练掌握协程都能大大提高我们的编程效率和代码质量。