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

Kotlin协程入门与基础概念剖析

2021-02-023.2k 阅读

Kotlin 协程基础概念

什么是协程

协程(Coroutine)在 Kotlin 中是一种轻量级的异步编程模型。与传统的线程不同,协程更像是一种用户态的轻量级线程,它允许我们以一种顺序、同步的方式编写异步代码。从本质上讲,协程是一种可以暂停和恢复执行的函数。

例如,我们在 Android 开发中可能需要进行网络请求,通常网络请求是异步的,因为它可能会花费较长时间,而使用协程可以让我们在代码编写上更接近同步方式,而不会阻塞主线程。

协程与线程的关系

线程是操作系统层面的概念,每个线程都有自己的栈空间,线程的创建和销毁开销相对较大。而协程是基于线程之上构建的,一个线程可以运行多个协程。协程的挂起和恢复并不涉及操作系统层面的线程切换,而是由 Kotlin 运行时库来管理,这使得协程的开销非常小。

想象一下,线程就像是一辆汽车,而协程则像是汽车里不同的乘客。一辆汽车(线程)可以搭载多个乘客(协程),每个乘客(协程)在适当的时候可以暂停自己的活动(挂起),让其他乘客(协程)有机会执行。

协程的优势

  1. 代码简洁性:使用协程可以避免复杂的回调嵌套,也就是俗称的 “回调地狱”。例如,在进行多个异步操作的链式调用时,传统的回调方式会使代码变得非常混乱,而协程可以让代码保持顺序执行的风格。
  2. 资源高效利用:由于协程是轻量级的,创建大量协程的开销远远小于创建大量线程。这在需要处理大量并发任务的场景下非常有用,比如在服务器端处理大量客户端连接时。
  3. 易于理解和维护:协程以同步的方式编写异步代码,开发人员可以更直观地理解代码的执行逻辑,从而降低代码维护的难度。

Kotlin 协程的基本使用

启动协程

在 Kotlin 中,我们可以使用 launch 函数来启动一个新的协程。launch 函数返回一个 Job 对象,通过这个对象我们可以控制协程的生命周期,比如取消协程。

以下是一个简单的示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        // 协程体
        println("Hello from coroutine")
    }
    job.join() // 等待协程执行完毕
    println("Job is completed")
}

在这个例子中,runBlocking 函数用于阻塞当前线程,直到其内部的协程执行完毕。launch 函数创建并启动了一个新的协程,协程体中的代码会在新的协程中执行。job.join() 用于等待协程执行结束,然后主线程继续执行打印 “Job is completed”。

协程作用域

协程作用域定义了协程的生命周期范围。launch 函数需要在一个协程作用域内调用。常见的协程作用域有 GlobalScoperunBlocking 创建的作用域。

GlobalScope 是一个全局的协程作用域,它的生命周期与整个应用程序相同。使用 GlobalScope 启动的协程在应用程序的整个生命周期内都会运行,即使启动它的函数已经返回。

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        println("Hello from GlobalScope coroutine")
    }
    println("Main function is completed")
}

在这个例子中,GlobalScope.launch 启动的协程是独立于 main 函数的,main 函数会直接打印 “Main function is completed” 然后结束,而协程可能还在后台执行。

runBlocking 创建的作用域则不同,它会阻塞当前线程,直到其内部的协程执行完毕。这在需要在主线程中等待协程结果的场景下很有用,比如在测试代码中。

协程的挂起函数

挂起函数是协程中的重要概念。挂起函数可以暂停协程的执行,并将控制权交回给调用者,同时保存协程的执行状态。当挂起函数恢复执行时,协程会从暂停的地方继续执行。

Kotlin 标准库提供了一些挂起函数,比如 delaydelay 函数会暂停当前协程指定的时间。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        println("Start delay")
        delay(2000) // 暂停 2 秒
        println("End delay")
    }
    println("Main function continues")
}

在这个例子中,launch 启动的协程会先打印 “Start delay”,然后调用 delay(2000) 暂停 2 秒,接着打印 “End delay”。而主线程不会被阻塞,会立即打印 “Main function continues”。

深入理解 Kotlin 协程的原理

协程的状态机

从本质上讲,协程是通过状态机来实现的。当协程执行到一个挂起函数时,它会保存当前的状态并暂停执行。Kotlin 编译器会将协程代码转换为状态机代码,每个状态代表协程执行到的不同阶段。

例如,考虑以下简单的协程代码:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        println("Step 1")
        delay(1000)
        println("Step 2")
    }
}

编译器会将这段代码转换为一个状态机,初始状态下协程执行到 println("Step 1"),当遇到 delay(1000) 时,协程保存当前状态(此时已经执行了 println("Step 1"))并暂停。1 秒后,协程恢复执行,从保存的状态继续,执行 println("Step 2")

协程上下文

协程上下文是一个包含了协程相关信息的集合,比如协程调度器、协程名称等。每个协程都有一个上下文,协程上下文可以在创建协程时指定。

协程调度器是协程上下文中非常重要的一部分,它决定了协程在哪个线程或线程池上执行。Kotlin 提供了几种不同的调度器,比如 Dispatchers.DefaultDispatchers.IODispatchers.Main

Dispatchers.Default 用于 CPU 密集型任务,它使用一个共享的线程池。Dispatchers.IO 用于 I/O 密集型任务,如文件读取、网络请求等,它也使用一个线程池,但优化了 I/O 操作。Dispatchers.Main 用于 Android 主线程,在 Android 开发中,更新 UI 的操作必须在主线程进行,所以涉及 UI 更新的协程需要使用这个调度器。

以下是一个指定调度器的示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.IO) {
        // 执行 I/O 操作,如文件读取或网络请求
        println("Running on ${Dispatchers.IO.name}")
    }
    launch(Dispatchers.Default) {
        // 执行 CPU 密集型任务
        println("Running on ${Dispatchers.Default.name}")
    }
    launch(Dispatchers.Main) {
        // 在 Android 主线程执行,用于 UI 更新
        println("Running on ${Dispatchers.Main.name}")
    }
}

在这个例子中,不同的协程通过指定不同的调度器,会在不同的线程环境中执行。

协程的生命周期管理

协程的生命周期由 Job 对象控制。Job 有几个重要的状态:New(新创建)、Active(活动中)、Completed(已完成)和 Cancelled(已取消)。

当我们使用 launch 函数创建协程时,协程处于 New 状态,一旦开始执行,就进入 Active 状态。当协程执行完毕(正常结束或抛出异常),会进入 Completed 状态。我们可以通过调用 job.cancel() 方法将协程状态设置为 Cancelled

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("Job is running: $i")
                delay(100)
            }
        } catch (e: CancellationException) {
            println("Job is cancelled")
        }
    }
    delay(500)
    job.cancel() // 取消协程
    job.join() // 等待协程结束
    println("Job is finally completed")
}

在这个例子中,协程开始运行后,会不断打印 “Job is running: $i”,每 100 毫秒打印一次。500 毫秒后,调用 job.cancel() 取消协程,协程捕获到 CancellationException 并打印 “Job is cancelled”,最后主线程打印 “Job is finally completed”。

Kotlin 协程的高级应用

协程的并发与并行

在 Kotlin 协程中,我们可以轻松实现并发和并行操作。并发是指在同一时间段内处理多个任务,而并行是指在同一时刻处理多个任务。

通过启动多个协程,我们可以实现并发操作。例如,我们有两个网络请求任务,我们可以启动两个协程同时执行这两个请求:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job1 = launch {
        // 模拟网络请求 1
        delay(2000)
        println("Network request 1 completed")
    }
    val job2 = launch {
        // 模拟网络请求 2
        delay(3000)
        println("Network request 2 completed")
    }
    job1.join()
    job2.join()
    println("All requests are completed")
}

在这个例子中,job1job2 两个协程并发执行,它们不会相互等待,各自执行自己的延迟操作,最后主线程等待两个协程都完成后打印 “All requests are completed”。

如果要实现并行操作,我们可以使用 Dispatchers.Default 调度器,并结合 async 函数。async 函数类似于 launch,但它返回一个 Deferred 对象,通过这个对象可以获取协程的执行结果。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result1 = async(Dispatchers.Default) {
        // 模拟 CPU 密集型任务 1
        (1..1000000).sum()
    }
    val result2 = async(Dispatchers.Default) {
        // 模拟 CPU 密集型任务 2
        (1000001..2000000).sum()
    }
    val total = result1.await() + result2.await()
    println("Total sum: $total")
}

在这个例子中,两个 async 启动的协程在 Dispatchers.Default 调度器的线程池上并行执行 CPU 密集型任务,最后将两个任务的结果相加并打印。

协程的异常处理

在协程中,异常处理与普通函数有所不同。当一个协程抛出异常时,如果没有被捕获,默认情况下会导致整个协程作用域取消。

我们可以使用 try - catch 块来捕获协程内部的异常:

import kotlinx.coroutines.*

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

在这个例子中,协程内部抛出 RuntimeException,通过 try - catch 块捕获并打印异常信息,主线程不受影响,继续打印 “Main function continues”。

另外,我们还可以使用 CoroutineExceptionHandler 来全局处理协程中的异常。CoroutineExceptionHandler 可以添加到协程上下文中,当协程抛出未捕获的异常时,会调用 CoroutineExceptionHandlerhandleException 方法。

import kotlinx.coroutines.*

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

fun main() = runBlocking(handler) {
    launch {
        throw RuntimeException("An error occurred")
    }
    println("Main function continues")
}

在这个例子中,CoroutineExceptionHandler 捕获到协程抛出的 RuntimeException,并打印异常信息,主线程同样继续执行。

协程的通道(Channel)

通道(Channel)是 Kotlin 协程中用于协程间通信的重要工具。它类似于生产者 - 消费者模型,一个协程可以作为生产者向通道发送数据,另一个协程可以作为消费者从通道接收数据。

以下是一个简单的通道示例:

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

fun main() = runBlocking {
    val channel = Channel<Int>()

    launch {
        // 生产者协程
        for (i in 1..5) {
            channel.send(i)
            println("Sent: $i")
        }
        channel.close()
    }

    launch {
        // 消费者协程
        for (number in channel) {
            println("Received: $number")
        }
        println("Channel is closed")
    }

    delay(1000)
}

在这个例子中,第一个 launch 启动的协程作为生产者,向通道发送 1 到 5 的整数,发送后打印 “Sent: $i”。第二个 launch 启动的协程作为消费者,从通道接收数据并打印 “Received: $number”。当生产者调用 channel.close() 关闭通道后,消费者的 for 循环结束,并打印 “Channel is closed”。

通道有不同的类型,比如 RendezvousChannelLinkedTransferChannelArrayChannel,它们在性能和行为上有所差异,可以根据具体需求选择合适的通道类型。

通过以上对 Kotlin 协程的基础概念、基本使用、原理以及高级应用的剖析,相信你对 Kotlin 协程已经有了较为深入的理解。在实际开发中,合理运用 Kotlin 协程可以极大地提高代码的异步处理能力和可读性,使开发更加高效。