参考:【Kotlin精简】第8章 协程
Kotlin 协程的基本概念及用法

在 Android 平台上,协程主要用来解决两个问题:

  1. 处理耗时任务 (Long running tasks),这种任务常常会阻塞住主线程;
  2. 保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数。

特点一句话总结:协程能更加安全实现异步代码同步化,实质是对线程切换的封装

1. 协程的基本使用

使用 launch 方法
协程在写法上和普通的顺序代码类似,可以让开发者用同步的方式写出异步的代码。创建协程可以使用以下三种方式:

runBlocking {
    // 方法1:使用 runBlocking 顶层函数
}
 
GlobalScope.launch {
    // 方法2:使用 GlobalScope 单例对象,调用 launch 开启协程
}
 
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    // 方法3:自行通过 CoroutineContext 创建一个 CoroutineScope 对象
}
  • 方法 1 适用于单元测试场景,实际开发中不使用,因为它是线程阻塞的;
  • 方法 2 与 runBlocking 相比不会阻塞线程,但它的生命周期会和 APP 一致,且无法取消;
  • 方法 3 比较推荐使用,可以通过 context 参数去管理和控制协程的生命周期。

此处的 launch 方法含义是:创建一个新的协程,并在指定的线程上运行它。传给 launch 方法的连续代码段就被叫做一个协程,传给 launch 方法的方法参数可以用于指定执行这段代码的线程。

coroutineScope.launch(Dispatchers.IO) {
    // 可以通过 Dispatchers.IO 参数把任务切到 IO 线程执行
}
 
coroutineScope.launch(Dispatchers.Main) {
    // 也可以通过 Dispatchers.Main 参数切换到主线程
}

使用 withContext 方法
这个方法可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切换回去继续执行,如下所示:

coroutineScope.launch(Dispatchers.Main) {        // 在 UI 线程开始
    val image = withContext(Dispatchers.IO) {    // 切换到 IO 线程
        getImage(imageId)                        // 在 IO 线程执行
    }
    imageView.setImageBitmap(image)              // 回到 UI 线程更新 UI
}

该方法支持自动切回原来的线程,能够消除并发代码在协作时产生的嵌套。如果需要频繁地进行线程切换,这种写法将有很大的优势,这就是「使用同步的方式写异步代码」。

使用 suspend 关键字
我们可以把 withContext 单独放进一个方法里面,此方法需要使用 suspend 关键字标记才能编译通过:

suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {}

使用 launch、async 等方法创建的协程,在执行到某个 suspend 方法时会从正在执行它的线程上脱离。互相脱离后的线程和协程将会分别执行不同的任务:

  • 线程:线程执行到了 suspend 方法,就暂时不再执行剩余协程代码,跳出协程的代码块。如果它是一个后台线程,它会被系统回收或者再利用(继续执行别的后台任务),与 Java 线程池中的线程等同;如果它是 Android 主线程,它会继续执行界面刷新任务。
  • 协程:协程会从上面被挂起的 suspend 方法开始,在该方法的参数指定的线程(如 Dispatchers.IO 所指定的 IO 线程)中继续往下执行。suspend 方法执行完成之后,会重新切换回它原先的线程。这个「切回来」的动作,在 Kotlin 中叫做 resume。

suspend 关键字只是一个提醒,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 方法。该关键字本身只有一个效果:限制这个方法只能在协程里或者另一个 suspend 方法中被调用,否则就会编译不通过。

获取协程的返回值
协程是一种异步的概念,需要一些特殊操作才能获取返回值。获取协程的返回值可以使用以下方式:
async / await
主要流程是使用 async 开启协程,然后调用 async 返回的 Deferred 对象的 await 方法获取协程运算的结果:

coroutineScope.launch(Dispatchers.IO) {
    val job = async {
        delay(1000)
        return@async "return value"
    }
    println("async result=${job.await()}")
}

suspendCoroutine
与 async 不同,suspendCoroutine 只是一个挂起方法,无法开启协程,需要在其他协程作用域中使用。协程运行结束后,使用 resume 提交返回值或使用 resumeWithException 抛出异常。

coroutineScope.launch(Dispatchers.IO) {
    try {
        val result = suspendCoroutine<String> {
            delay(1000)
            val random = Random().nextBoolean()
            if (random) {
                it.resume("return value")
            } else {
                it.resumeWithException(Exception("Coroutine Failure"))
            }
        }
        println("suspendCoroutine success result: $result")
    } catch (e: java.lang.Exception) {
        println("suspendCoroutine failure exception: $e")
    }
}

协程的非阻塞式挂起
「非阻塞式挂起」指的就是协程在挂起的同时切线程这件事情。使用了协程的代码看似阻塞,但由于协程内部做了很多工作(包括自动切换线程),它实际上是非阻塞的。在代码执行的过程中,线程虽然会切换,但写法上类似普通的单线程代码。
在 Kotlin 中,协程就是基于线程来实现的一种更上层的工具 API,类似于 Android 自带的 Handler 系列 API。在设计思想上,协程是一个基于线程的上层框架。Kotlin 协程并没有脱离 Kotlin 或者 JVM 创造新的东西,只是简化了多线程的开发。

代码示例
使用协程模拟实现一个网络请求,等待时显示 Loading,请求成功或者出错让 Loading 消失,并将状态反馈给用户。
在 ViewModel 中编写如下业务逻辑代码:

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
    enum class RequestStatus {
        IDLE, LOADING, SUCCESS, FAIL
    }
    
    val requestStatus = MutableStateFlow(RequestStatus.IDLE)
    
    /**
     * 模拟网络请求,并将状态设置给 requestStatus 变量
     */
    fun simulateNetworkRequest() {
        requestStatus.value = RequestStatus.LOADING
        viewModelScope.launch {
            val requestResult = async { performSimulatedRequest() }.await()
            requestStatus.value = if (requestResult) RequestStatus.SUCCESS else RequestStatus.FAIL
        }
    }
    
    /**
     * 使用 delay 方法模拟耗时操作,用随机数模拟请求成功或失败
     */
    private suspend fun performSimulatedRequest() = withContext(Dispatchers.IO) {
        delay(500)
        val random = Random()
        return@withContext random.nextBoolean()
    }
}

MainActivity 中使用 Jetpack Compose,将请求状态实时显示在界面上:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    private val mainViewModel: MainViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    val requestStatusState = mainViewModel.requestStatus.collectAsState()
                    val requestStatus by rememberSaveable { requestStatusState }
                    
                    Text(
                        text = requestStatus.name,
                        color = Color.Red
                    )
                }
            }
        }
        mainViewModel.simulateNetworkRequest()
    }
}

2. 协程构建器:launch vs async

launch:

  • 用于启动一个“即发即忘”的协程,该协程执行一个任务但不直接返回结果。
  • 返回一个 Job 对象,用于管理协程的生命周期(取消、等待完成)。
  • 内部未捕获的异常会传递给作用域的 Job,可能触发取消或由 CoroutineExceptionHandler 处理。

async:

  • 用于启动一个需要计算结果的协程。
  • 返回一个 Deferred 对象(本质上是带有结果的 Job)。
  • 在需要结果的地方,调用 Deferred.await()(一个 suspend 函数)来挂起当前协程,直到 async 协程完成并返回结果或抛出异常。
  • 内部未捕获的异常在调用 await() 时才会抛出给调用者。
  • 关键点: async 本身是立即启动的。await() 是挂起点。

3. 协程面试题

1、什么是 Kotlin 协程?
Kotlin 协程是一种轻量级的并发框架,用于简化异步编程。它允许开发者使用顺序的方式来编写异步的、非阻塞的代码,提供了一种能够挂起和恢复执行的机制。

2、Kotlin 协程与线程的区别是什么?
Kotlin 协程是基于线程的,但是它们更轻量级、更高效。线程是操作系统调度的最小执行单位,而协程是在运行时进行调度的,可以允许更多的协程在较少的线程上执行。

3、如何创建一个协程?
可以使用 launch 函数或async函数来创建一个协程。例如,launch { … } 可以创建一个顶层协程,它将在协程作用域中运行。

4、协程的取消机制是什么?
协程的取消可以通过调用 cancel 方法或者取消相关的协程作用域来实现。协程会在取消后立即停止执行,并调用相应的取消回调。

5、如何处理协程中的异常?
可以使用 try/catch 块来捕获协程中的异常。可以使用 CoroutineExceptionHandler 来设置一个统一的异常处理程序。

6、什么是挂起函数?
挂起函数是指在执行期间可能会暂停执行的函数。它们通过使用 suspend 修饰符来定义,可以被其他协程挂起和恢复执行。

7、协程的调度器是什么?
协程的调度器是负责决定协程在哪个线程上执行的组件。Kotlin 协程的调度器可以通过 launch、async 等函数的参数来指定,也可以使用 withContext 函数在协程内部切换调度器。

8、协程的上下文是什么?
协程的上下文是一组键值对,包含了协程的调度器、异常处理器等信息。可以使用 CoroutineScope 或者 coroutineScope函数来创建具有特定上下文的协程作用域。

9、协程的并发与并行有何区别?
协程的并发是指在同一个线程上进行交替执行的能力,通过使用协程挂起和恢复执行的机制来实现。而并行是指在不同的线程上同时执行多个任务。

10、什么是协程的父子关系?
协程可以嵌套在其他协程中,形成父子关系。父协程在执行时会等待其所有子协程执行完毕,这样可以实现更好的结构化并发。

11、协程的优势有哪些?
协程具有以下优势:

简化异步编程,用顺序代码编写异步逻辑
更轻量级,可以在较少的线程上运行大量的协程
提供异常处理机制,使得错误处理更加灵活
支持结构化并发,提高代码的可读性和可维护性

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐