从零开始学Kotlin协程:快速上手指南 – wiki基地

从零开始学 Kotlin 协程:快速上手指南

协程是 Kotlin 中一个强大且令人兴奋的特性,它极大地简化了异步编程,让你可以用同步的方式编写非阻塞的代码。这听起来有点矛盾,但正是协程的魅力所在。如果你之前被回调地狱、复杂的线程管理所困扰,那么协程绝对值得你花时间学习。

本文将带你从零开始,一步步了解 Kotlin 协程的核心概念、基本用法,以及一些常见的应用场景。让我们开始这段协程之旅吧!

1. 为什么需要协程?

在深入协程的细节之前,我们先来思考一个问题:为什么我们需要协程?或者说,传统的异步编程方式有什么问题?

传统的异步编程方式主要有以下几种:

  • 回调(Callbacks): 这是最基本的方式,通过传递回调函数来处理异步操作的结果。但是,当异步操作嵌套层数较多时,就会形成“回调地狱”,代码可读性和可维护性极差。
  • Future/Promise: 这种方式将异步操作的结果封装成一个 Future 或 Promise 对象,可以通过链式调用来处理结果。相比回调,它稍微好一些,但仍然不够简洁。
  • 线程(Threads): 通过创建新的线程来执行耗时操作,避免阻塞主线程。但是,线程的创建和销毁开销较大,而且线程数量过多时,会导致线程上下文切换频繁,降低系统性能。

这些方式都存在一些问题,要么是代码难以阅读和维护,要么是资源消耗较大。而协程的出现,正是为了解决这些问题。

协程的优势:

  • 简化异步编程: 协程让你以同步的方式编写异步代码,避免了回调地狱和复杂的线程管理。
  • 轻量级: 协程非常轻量级,创建和切换的开销远小于线程。你可以在一个线程中创建数百万个协程,而不会导致系统资源耗尽。
  • 非阻塞: 协程的挂起和恢复是非阻塞的,不会阻塞线程。当一个协程挂起时,线程可以去执行其他任务,从而提高系统的吞吐量。
  • 结构化并发: 协程提供了结构化的并发机制,可以更好地控制协程的生命周期,避免资源泄漏和协程失控。

2. 协程的核心概念

要理解协程,你需要掌握以下几个核心概念:

  • 协程(Coroutine): 协程本质上是一种轻量级的线程。它可以挂起(suspend)和恢复(resume)执行,而不会阻塞线程。你可以将协程理解为一个可以暂停和继续执行的任务。
  • 挂起函数(Suspending Function): 挂起函数是协程的核心。它使用 suspend 关键字修饰,表示这个函数可以被挂起。挂起函数只能在协程或其他挂起函数中调用。
  • 协程构建器(Coroutine Builder): 协程构建器是用来启动协程的函数。常见的协程构建器有 launchasyncrunBlocking
  • 协程作用域(Coroutine Scope): 协程作用域定义了协程的生命周期。它负责管理协程的启动、取消和异常处理。
  • 调度器(Dispatcher): 调度器决定了协程在哪个线程上执行。Kotlin 提供了几种内置的调度器,如 Dispatchers.DefaultDispatchers.IODispatchers.Main
  • 上下文(Context): 协程上下文是一个键值对集合,用于存储协程相关的信息,如调度器、作业(Job)和异常处理器。
  • Job 和 DeferredJob 对象代表一个协程, 可以用于取消协程。async 构建器会返回一个 Deferred 对象, 它继承自 Job, 并且有一个 await() 方法, 用于等待协程执行完成并获取结果。

3. 第一个协程程序

让我们从一个简单的例子开始,看看如何创建一个协程:

“`kotlin
import kotlinx.coroutines.*

fun main() {
println(“Start”)

// 使用 launch 启动一个协程
GlobalScope.launch {
    delay(1000) // 模拟耗时操作,非阻塞
    println("Hello from coroutine!")
}

println("End")
Thread.sleep(2000) // 阻塞主线程,等待协程执行完毕

}
“`

代码解释:

  1. GlobalScope.launch { ... }:使用 launch 协程构建器启动一个协程。GlobalScope 表示协程的生命周期与整个应用程序相同。
  2. delay(1000):这是一个挂起函数,它会暂停协程 1 秒钟,但不会阻塞线程。
  3. Thread.sleep(2000):这里使用 Thread.sleep 阻塞主线程 2 秒钟,是为了让协程有足够的时间执行完毕。否则,主线程会立即结束,协程可能还没来得及执行。

运行结果:

Start
End
Hello from coroutine!

可以看到,"End" 先于 "Hello from coroutine!" 输出,这说明 delay(1000) 并没有阻塞主线程。

注意: 在实际开发中,通常不建议使用 GlobalScope,因为它创建的协程生命周期过长,容易导致资源泄漏。我们会在后面介绍更合适的协程作用域。

4. 协程构建器详解

Kotlin 提供了几种协程构建器,它们各有特点和适用场景:

  • launch 启动一个协程,但不返回任何结果。它返回一个 Job 对象,可以用于取消协程。launch 通常用于执行 fire-and-forget 类型的任务。

  • async 启动一个协程,并返回一个 Deferred 对象。Deferred 是一个带有结果的 Job,你可以通过调用 await() 方法来获取协程的执行结果。async 通常用于执行需要返回结果的异步任务。

  • runBlocking 阻塞当前线程,直到协程执行完毕。它通常用于连接阻塞代码和挂起代码,例如在 main 函数中启动协程,或者在单元测试中测试挂起函数。

示例:

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking { // 使用 runBlocking 阻塞主线程
println(“Start”)

// 使用 launch 启动一个协程
val job = launch {
    delay(1000)
    println("Hello from launch!")
}

// 使用 async 启动一个协程
val deferred = async {
    delay(2000)
    "Hello from async!"
}

println("Waiting for results...")
println("Result from async: ${deferred.await()}") // 等待 async 协程的结果

job.join() // 等待 launch 协程执行完毕
println("End")

}
“`

代码解释:

  1. runBlocking { ... }:使用 runBlocking 阻塞主线程,直到其中的协程执行完毕。
  2. launch { ... }:启动一个不需要返回结果的协程。
  3. async { ... }:启动一个需要返回结果的协程。
  4. deferred.await():等待 async 协程执行完毕,并获取其结果。
  5. job.join():等待 launch 协程执行完毕。

运行结果:

Start
Waiting for results...
Hello from launch!
Hello from async!
Result from async: Hello from async!
End

5. 协程作用域

协程作用域(Coroutine Scope)定义了协程的生命周期。它负责管理协程的启动、取消和异常处理。

为什么需要协程作用域?

  • 结构化并发: 协程作用域提供了一种结构化的并发机制,可以更好地控制协程的生命周期,避免资源泄漏和协程失控。
  • 自动取消: 当协程作用域被取消时,它会自动取消其所有子协程。
  • 异常处理: 协程作用域可以捕获和处理协程中未捕获的异常。

常见的协程作用域:

  • GlobalScope 全局作用域,协程的生命周期与整个应用程序相同。通常不建议使用,容易导致资源泄漏。
  • CoroutineScope(context) 创建一个自定义的协程作用域。你需要传入一个协程上下文(Coroutine Context)作为参数。
  • lifecycleScope (Android): Android 架构组件 Lifecycle 提供的协程作用域,协程的生命周期与 Lifecycle 组件(如 ActivityFragment)绑定。
  • viewModelScope (Android): Android 架构组件 ViewModel 提供的协程作用域,协程的生命周期与 ViewModel 绑定。

示例(Android):

“`kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 使用 lifecycleScope 启动协程
    lifecycleScope.launch {
        delay(1000)
        println("Hello from lifecycleScope!")
    }
}

}
“`

代码解释:

  • lifecycleScope.launch { ... }:使用 lifecycleScope 启动一个协程。当 MainActivity 被销毁时,lifecycleScope 会自动取消其所有子协程。

6. 调度器

调度器(Dispatcher)决定了协程在哪个线程上执行。Kotlin 提供了几种内置的调度器:

  • Dispatchers.Default 默认调度器,适用于 CPU 密集型任务,如计算、排序等。它使用一个共享的线程池。
  • Dispatchers.IO IO 调度器,适用于 IO 密集型任务,如网络请求、文件读写等。它也使用一个共享的线程池,但线程池的大小可以根据需要动态调整。
  • Dispatchers.Main 主线程调度器,适用于更新 UI。在 Android 中,它对应于主线程(UI 线程)。
  • Dispatchers.Unconfined: 非受限调度器。 它在调用者的线程中启动协程, 但是仅仅会持续到第一个挂起点。 在挂起之后, 它在相应的挂起函数使用的任何线程中恢复协程。 应该避免使用,除非为了达到某些特殊目的.

示例:

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
launch(Dispatchers.Default) {
println(“Default: ${Thread.currentThread().name}”)
}

launch(Dispatchers.IO) {
    println("IO: ${Thread.currentThread().name}")
}

launch(Dispatchers.Main) { // 如果在非主线程中运行,会抛出异常
    println("Main: ${Thread.currentThread().name}")
}

}
“`

代码解释:

  • launch(Dispatchers.Default):在默认调度器上启动协程,通常是后台线程。
  • launch(Dispatchers.IO):在 IO 调度器上启动协程,通常也是后台线程。
  • launch(Dispatchers.Main):在主线程调度器上启动协程。

运行结果(可能因环境而异):

Default: DefaultDispatcher-worker-1
IO: DefaultDispatcher-worker-2
Exception in thread "main" java.lang.IllegalStateException: ...

如果在非主线程中运行,会抛出Dispatchers.Main相关的异常,这是因为Dispatchers.Main只能在主线程(例如Android的UI线程)中使用。

7. 挂起函数

挂起函数是协程的核心。它使用 suspend 关键字修饰,表示这个函数可以被挂起。挂起函数只能在协程或其他挂起函数中调用。

挂起函数的特点:

  • 非阻塞: 挂起函数在执行到挂起点时,会暂停协程的执行,但不会阻塞线程。线程可以去执行其他任务。
  • 可恢复: 当挂起的操作完成后,协程会在挂起点恢复执行。
  • 只能在协程中调用: 挂起函数只能在协程或其他挂起函数中调用。

示例:

“`kotlin
import kotlinx.coroutines.*

suspend fun doSomethingUsefulOne(): Int {
delay(1000) // 模拟耗时操作
return 13
}

suspend fun doSomethingUsefulTwo(): Int {
delay(2000) // 模拟耗时操作
return 29
}

fun main() = runBlocking {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println(“The answer is ${one.await() + two.await()}”)
}
println(“Completed in $time ms”)
}
“`

代码解释:

  • suspend fun doSomethingUsefulOne()suspend fun doSomethingUsefulTwo():定义了两个挂起函数,它们分别模拟了两个耗时操作。
  • async { ... }:使用 async 启动两个协程,分别执行 doSomethingUsefulOne()doSomethingUsefulTwo()
  • one.await()two.await():等待两个协程执行完毕,并获取其结果。
  • measureTimeMillis: 用于测量代码执行时间

运行结果:

The answer is 42
Completed in 2017 ms

可以看到,两个协程是并发执行的,总耗时大约是 2 秒,而不是 3 秒。

8. 协程的取消

协程是可以被取消的。你可以通过调用 Job 对象的 cancel() 方法来取消协程。

取消的原理:

  • 协程的取消是协作式的。也就是说,协程需要主动检查自己的取消状态,才能响应取消请求。
  • Kotlin 提供了一些内置的挂起函数,如 delayyield 等,它们会在挂起时检查协程的取消状态。如果协程已经被取消,这些函数会抛出 CancellationException
  • 你也可以通过 isActive 属性来检查协程的取消状态。

示例:

“`kotlin
import kotlinx.coroutines.*

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.cancel() // 取消协程
job.join() // 等待协程执行完毕
println(“main: Now I can quit.”)
}
“`

代码解释:

  • job.cancel():取消协程。
  • job.join():等待协程执行完毕。由于协程已经被取消,join() 会立即返回。
  • delay 函数会检查协程的取消状态。

运行结果:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

可以看到,协程在执行到第三次循环时被取消了。

处理 CancellationException

通常情况下,你不需要显式地处理 CancellationException。协程的取消机制会自动处理它。但是,如果你需要在协程被取消时执行一些清理操作,可以使用 try ... finally 块。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println(“job: I’m sleeping $i …”)
delay(500L)
}
} finally {
println(“job: I’m running finally”)
}
}
delay(1300L) // 延迟一段时间
println(“main: I’m tired of waiting!”)
job.cancelAndJoin() // 取消并等待协程
println(“main: Now I can quit.”)
}

“`

代码解释:
* cancelAndJoin():这是一个方便的函数,它等价于先调用 cancel(),再调用 join()

运行结果
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.

9. 异常处理

协程中的异常处理与普通函数的异常处理类似,可以使用 try ... catch 块来捕获和处理异常。

未捕获的异常:

  • 如果协程中抛出了一个未捕获的异常,它会沿着协程的层次结构向上传播,直到被某个协程作用域捕获或导致应用程序崩溃。
  • launch 构建器创建的协程,如果发生未捕获异常,默认会打印异常堆栈并终止程序。
  • async 构建器创建的协程,如果发生未捕获异常,异常会被包装在 Deferred 对象中,当你调用 await() 方法时,会重新抛出该异常。

使用 CoroutineExceptionHandler

你可以创建一个 CoroutineExceptionHandler 对象,并将其传递给协程作用域,来处理协程中未捕获的异常。

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println(“Caught $exception”)
}

val job = GlobalScope.launch(handler) {
    throw AssertionError("Something went wrong")
}

job.join()

}
“`

代码解释:

  • CoroutineExceptionHandler { _, exception -> ... }:创建一个异常处理器,它会打印捕获到的异常。
  • GlobalScope.launch(handler) { ... }:使用 launch 启动一个协程,并将异常处理器传递给它。

运行结果:

Caught java.lang.AssertionError: Something went wrong

10. 总结

本文详细介绍了 Kotlin 协程的基础知识,包括核心概念、基本用法、协程构建器、协程作用域、调度器、挂起函数、协程的取消和异常处理。

掌握这些知识,你已经可以开始使用协程来简化你的异步编程代码了。当然,协程还有更多高级特性,如通道(Channel)、流(Flow)等,等待你去探索。

希望这篇文章能帮助你快速上手 Kotlin 协程!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部