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

Kotlin协程与异步编程

2022-04-213.6k 阅读

Kotlin 协程基础

Kotlin 协程是一种轻量级的异步编程模型,它允许开发者以一种更简洁、更直观的方式编写异步代码。与传统的异步编程方式(如回调、Future)相比,协程提供了一种类似同步代码的编写风格,使得异步代码更易于理解和维护。

协程的概念

协程可以看作是一种用户态的轻量级线程。与操作系统线程不同,协程的调度是由用户代码控制的,而不是由操作系统内核调度。这意味着协程的创建、挂起和恢复都非常轻量级,开销远远小于传统线程。

启动协程

在 Kotlin 中,我们可以使用 launch 函数来启动一个新的协程。launch 函数接受一个 CoroutineScope 和一个 suspend 函数作为参数。CoroutineScope 用于管理协程的生命周期,而 suspend 函数则是协程的执行体。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        // 这是协程的执行体
        println("Hello from coroutine")
    }
    println("Hello from main")
}

在上述代码中,runBlocking 函数用于阻塞当前线程,直到其内部的所有协程执行完毕。launch 函数启动了一个新的协程,协程执行体打印出 "Hello from coroutine",而主线程继续执行并打印出 "Hello from main"。由于协程的异步特性,这两个打印语句的执行顺序是不确定的。

协程的生命周期

协程有几个重要的状态:新建(New)、就绪(Active)、挂起(Suspended)、完成(Completed)和取消(Cancelled)。

  1. 新建:当使用 launch 或其他创建协程的函数创建一个协程时,协程处于新建状态。此时协程尚未开始执行。
  2. 就绪:协程一旦被调度执行,就进入就绪状态。在这个状态下,协程开始执行其 suspend 函数体。
  3. 挂起:当协程执行到一个 suspend 函数时,它会暂停执行,并将控制权交回给调用者。此时协程进入挂起状态。
  4. 完成:当协程的 suspend 函数体执行完毕,或者因为异常而终止时,协程进入完成状态。
  5. 取消:可以通过调用 cancel 函数来取消一个协程。取消后的协程会立即停止执行,并进入取消状态。

挂起函数

什么是挂起函数

挂起函数是 Kotlin 协程的核心概念之一。挂起函数是一种特殊的函数,它可以暂停协程的执行,并将控制权交回给调用者。挂起函数必须在协程内部或其他挂起函数中调用。

定义挂起函数

定义一个挂起函数非常简单,只需要在函数声明前加上 suspend 关键字。

suspend fun delayMessage(message: String) {
    delay(1000)
    println(message)
}

在上述代码中,delayMessage 是一个挂起函数,它内部调用了 delay 函数。delay 函数也是一个挂起函数,它会暂停当前协程的执行指定的时间(这里是 1000 毫秒)。

调用挂起函数

挂起函数必须在协程内部或其他挂起函数中调用。

fun main() = runBlocking {
    launch {
        delayMessage("Message after delay")
    }
}

在上述代码中,launch 启动的协程内部调用了 delayMessage 挂起函数。

协程上下文与调度器

协程上下文

协程上下文是一个包含了协程相关信息的集合,如协程的调度器、协程的名称、异常处理器等。每个协程都有一个关联的协程上下文。

调度器

调度器决定了协程在哪个线程或线程池中执行。Kotlin 提供了几种内置的调度器:

  1. Dispatchers.Default:用于 CPU 密集型任务,它使用一个共享的线程池。
  2. Dispatchers.IO:用于 I/O 密集型任务,它也使用一个共享的线程池,但优化了 I/O 操作。
  3. Dispatchers.Main:用于 Android 应用的主线程,只能在 Android 平台上使用。

设置调度器

可以通过在 launch 或其他启动协程的函数中指定 Dispatchers 来设置协程的调度器。

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        // 这个协程将在 Default 调度器的线程池中执行
        println("Running on ${Thread.currentThread().name}")
    }
    launch(Dispatchers.IO) {
        // 这个协程将在 IO 调度器的线程池中执行
        println("Running on ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Main) {
        // 在 Android 平台上,这个协程将在主线程执行
        println("Running on ${Thread.currentThread().name}")
    }
}

协程的并发与并行

并发

并发是指在同一时间段内处理多个任务,但不一定是同时执行。在协程中,通过启动多个协程,可以实现并发执行。

fun main() = runBlocking {
    launch {
        repeat(3) {
            println("Coroutine 1: $it")
            delay(100)
        }
    }
    launch {
        repeat(3) {
            println("Coroutine 2: $it")
            delay(100)
        }
    }
}

在上述代码中,启动了两个协程,它们并发执行,打印出不同的信息。

并行

并行是指在同一时刻同时执行多个任务。要实现并行,需要使用多个线程。Kotlin 协程通过调度器,可以在多个线程上并行执行协程。

fun main() = runBlocking {
    val job1 = launch(Dispatchers.Default) {
        repeat(3) {
            println("Parallel Coroutine 1: $it")
            delay(100)
        }
    }
    val job2 = launch(Dispatchers.Default) {
        repeat(3) {
            println("Parallel Coroutine 2: $it")
            delay(100)
        }
    }
    job1.join()
    job2.join()
}

在上述代码中,两个协程通过 Dispatchers.Default 调度器在不同的线程上并行执行。

异步编程中的回调地狱与协程的优势

回调地狱

在传统的异步编程中,我们经常使用回调函数来处理异步操作的结果。当有多个异步操作相互依赖时,代码会变得非常复杂,形成所谓的 "回调地狱"。

// Java 中的回调地狱示例
someAsyncOperation1(result1 -> {
    someAsyncOperation2(result1, result2 -> {
        someAsyncOperation3(result2, result3 -> {
            // 处理最终结果
        });
    });
});

这种嵌套的回调结构使得代码难以阅读和维护。

协程的优势

Kotlin 协程通过提供一种类似同步代码的编写风格,有效地解决了回调地狱的问题。

suspend fun asyncOperations() {
    val result1 = someAsyncOperation1()
    val result2 = someAsyncOperation2(result1)
    val result3 = someAsyncOperation3(result2)
    // 处理最终结果
}

在上述 Kotlin 代码中,异步操作以一种顺序的方式编写,就像同步代码一样,使得代码更易于理解和维护。

协程与 Future

Future 的概念

Future 是 Java 中用于表示异步操作结果的一种机制。通过 Future,我们可以获取异步操作的结果,或者检查异步操作是否完成。

Future 的使用示例

// Java 中使用 Future 的示例
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
    // 模拟一个耗时操作
    Thread.sleep(1000);
    return 42;
});
try {
    Integer result = future.get();
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    executor.shutdown();
}

协程与 Future 的对比

  1. 代码风格:协程提供了一种更简洁、更直观的类似同步代码的编写风格,而 Future 需要通过 get 方法阻塞等待结果,代码更偏向于异步回调风格。
  2. 性能:协程是轻量级的,创建和销毁的开销很小,而 Future 基于线程池,线程的创建和销毁开销较大。
  3. 错误处理:协程可以使用 try - catch 块来处理异常,就像同步代码一样,而 Future 需要在 get 方法调用处捕获异常,不够直观。

协程中的异常处理

未捕获异常

在协程中,如果一个未捕获的异常发生,默认情况下,协程会终止,并将异常传播到其 CoroutineScope

fun main() = runBlocking {
    launch {
        throw RuntimeException("Unhandled exception")
    }
    println("This will still be printed")
}

在上述代码中,协程抛出了一个未捕获的异常,但主线程不会受到影响,仍然会打印出 "This will still be printed"。

捕获异常

可以使用 try - catch 块来捕获协程中的异常。

fun main() = runBlocking {
    launch {
        try {
            throw RuntimeException("Handled exception")
        } catch (e: RuntimeException) {
            println("Caught exception: $e")
        }
    }
}

在上述代码中,协程中的异常被 try - catch 块捕获并处理。

全局异常处理

可以通过设置 CoroutineExceptionHandler 来全局处理协程中的异常。

val handler = CoroutineExceptionHandler { _, exception ->
    println("Global exception handler: $exception")
}

fun main() = runBlocking(handler) {
    launch {
        throw RuntimeException("Global exception")
    }
}

在上述代码中,CoroutineExceptionHandler 会捕获协程中抛出的异常并进行处理。

协程的高级特性

异步流(Flow)

Flow 是 Kotlin 协程中的一种异步数据流,它类似于 RxJava 中的 Observable。Flow 可以发射多个值,并在协程中进行处理。

import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    flow {
        for (i in 1..3) {
            emit(i)
        }
    }.collect { value ->
        println("Collected: $value")
    }
}

在上述代码中,flow 构建器创建了一个发射 1 到 3 的数据流,collect 函数用于收集并处理这些值。

通道(Channel)

通道是协程之间进行通信的一种机制。它类似于生产者 - 消费者模型,一个协程可以作为生产者向通道发送数据,另一个协程可以作为消费者从通道接收数据。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (i in 1..3) {
            channel.send(i)
        }
        channel.close()
    }
    launch {
        for (value in channel) {
            println("Received: $value")
        }
    }
}

在上述代码中,一个协程向 Channel 发送数据,另一个协程从 Channel 接收数据。

协程的组合与复用

可以通过组合多个协程来实现更复杂的异步操作。例如,可以使用 async 函数启动一个异步操作,并使用 await 函数获取其结果。

fun main() = runBlocking {
    val result1 = async { someAsyncOperation1() }
    val result2 = async { someAsyncOperation2(result1.await()) }
    val finalResult = result2.await()
    println("Final result: $finalResult")
}

在上述代码中,async 启动的两个协程相互依赖,通过 await 函数获取前一个协程的结果并继续执行。

Kotlin 协程在 Android 开发中的应用

主线程更新 UI

在 Android 开发中,更新 UI 必须在主线程进行。Kotlin 协程的 Dispatchers.Main 调度器可以很方便地实现这一点。

import android.os.Bundle
import android.widget.Button
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 button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)

        button.setOnClickListener {
            GlobalScope.launch(Dispatchers.IO) {
                // 模拟一个耗时操作
                delay(2000)
                val result = "Result after delay"
                withContext(Dispatchers.Main) {
                    textView.text = result
                }
            }
        }
    }
}

在上述代码中,按钮点击后,在 Dispatchers.IO 调度器上执行一个耗时操作,然后通过 withContext(Dispatchers.Main) 将结果更新到 UI 上。

处理网络请求

Kotlin 协程与 Retrofit 结合可以很方便地处理网络请求。

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import kotlinx.coroutines.*

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

data class User(val id: Int, val name: String)

object ApiClient {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
           .baseUrl(BASE_URL)
           .addConverterFactory(GsonConverterFactory.create())
           .build()
    }
    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
}

fun main() = runBlocking {
    val users = ApiClient.apiService.getUsers()
    users.forEach { user ->
        println("User: ${user.name}")
    }
}

在上述代码中,通过 Retrofit 和 Kotlin 协程实现了一个简单的网络请求,并处理了返回的数据。

通过以上对 Kotlin 协程与异步编程的深入介绍,相信你对 Kotlin 协程在异步编程中的应用有了更全面的了解。无论是处理简单的异步任务,还是复杂的并发和并行操作,Kotlin 协程都提供了强大而简洁的解决方案。在实际开发中,合理运用 Kotlin 协程可以提高代码的可读性、可维护性和性能。