Kotlin 协程 — 基础
协程已经存在一段时间了,关于它的各种文章也很多。但我发现想要了解它还比较费时,所以我花了一段时间才真正理解了协程的基础知识以及它的工作原理。因此,我想分享一些我理解到的内容。
什么是协程?
协程代表合作函数。它们提供了一种更有效和易读的方式来处理异步任务。它与线程类似,因为它需要一块代码来与其余代码同时运行。然而,协程并不绑定到任何特定线程。它可以在一个线程中暂停执行,并在另一个线程中恢复。协程在 Kotlin 1.3 版本中推出。
协程的优势
轻量级 — 我们可以在单个线程上运行许多协程,这归功于其对挂起的支持。挂起意味着你可以执行一些指令,然后在执行中间停止协程,并在需要时继续。挂起节省了内存,同时支持许多并发操作,而不是阻塞。
减少内存泄漏 — 协程遵循结构化并发原则,这意味着每个协程应该在具有确定生命周期的特定上下文中启动。结构化并发是一种方法,其中协程的生命周期与特定作用域绑定,确保在作用域本身完成之前,该作用域内启动的所有协程都已完成。这有助于避免协程泄漏并简化资源管理。
在 Android 上提供主线程安全性 — 协程帮助管理可能阻塞主线程的长时间运行任务,从而使应用程序变得无响应。主线程安全性允许你确保任何挂起函数都可以从主线程调用。
内置取消支持 — 协程最重要的机制之一是取消,因为在 Android 上,几乎每个协程都与某个视图关联,如果这个视图被销毁了,它的协程就不需要了,所以应该被取消。这是以前需要开发者付出很多努力的关键功能,但协程提供了一个简单和安全的取消机制。
协作式多任务处理 — 这意味着协程是一组通过协作执行一系列指令的并发原语,操作系统不控制由协程执行的任务或进程的调度。相反,它依赖于运行它们的程序和平台来执行该操作。因此,协程可以交回控制权给调度器,以允许其他线程运行。操作系统的调度器负责让这些线程执行它们的工作,如果需要,也可以暂停它们,以便其他线程可以使用相同的资源。
协程的关键词
这些是你在学习协程时会遇到的一些常见关键词。
suspend functions
Coroutine Scope (includes Dispatchers, Job)
Coroutine builders
Coroutine Context
suspend
函数
挂起函数是指那些可以暂停并在以后继续的函数。挂起函数表明该函数可以被挂起,允许其他协程在它等待非阻塞操作完成时运行。当挂起函数执行时,协程释放了它正在运行的线程,并允许其他协程访问该线程(因为协程是协作式的)。
挂起函数的语法与普通函数相同,只是加上了 suspend
关键字。挂起函数只允许从协程或其他挂起函数中调用。
suspend fun doSomething(): Int {
delay(1000L) // 假设我们在这里做一些有用的事
return 13
}
协程作用域
协程作用域定义了协程的生命周期/时长。它负责控制一组协程及其上下文的生命周期。一个 CoroutineScope
跟踪它创建的所有协程。因此,如果你取消了一个作用域,你就取消了它创建的所有协程。当子协程在父协程内启动时,它继承了父作用域(除非另有说明),以便当父协程停止时,子协程也会停止。
在 Android 中,协程有三个基本作用域:
全局作用域 (
GlobalScope
): 全局作用域是一个预定义的协程作用域,持续整个应用程序的生命周期。虽然它可能很方便,但通常建议使用自定义协程作用域以确保结构化并发。GlobalScope.launch { val config = fetchConfigFromServer() // 网络请求 updateConfiguration(config) }
生命周期作用域 (
LifecycleScope
): 绑定到LifecycleOwner
(如 Fragment 或 Activity)的生命周期。当 Fragment 或 Activity 被销毁时,这个作用域中的协程也会被取消。使用LifecycleScope
我们还可以利用特殊的启动条件:lifecycleScope.launchWhenResumed { println("loading..") delay(3000) println("job is done") }
launchWhenCreated
会在生命周期至少处于创建状态时启动协程,并在销毁状态时挂起。launchWhenStarted
会在生命周期至少处于开始状态时启动协程,并在停止状态时挂起。launchWhenResumed
会在生命周期至少处于恢复状态时启动协程,并在暂停状态时挂起。
ViewModel 作用域 (
ViewModelScope
): 绑定到ViewModel
的生命周期。当 ViewModel 清除时,这个作用域中的协程也会被取消。viewModelScope.launch { println("loading..") delay(3000) println("job is done") }
协程构建器
协程构建器是用于初始化或创建新协程的函数。它们提供了一种方便的方式来启动和控制协程的执行。
launch: 启动一个新的协程并发执行,即不阻塞当前线程。它自动在生成的工作被取消时被取消,并且它不返回任何结果。
launch
的返回类型是Job
。这意味着你可以通过与该工作交互来控制协程的生命周期。你可以通过调用job.cancel()
轻松地取消它。在 ViewModel 中经常使用launch
来从非挂起代码创建一个桥接到挂起代码。launch { delay(1000L) println("Hello World!") }
runBlocking: 运行一个新的协程并阻塞当前线程直到其完成。换句话说,运行它的线程在
runBlocking
的括号内的所有代码块完成执行之前被阻塞。fun main() = runBlocking { // this: CoroutineScope doWorld() } suspend fun doWorld() { delay(1000L) println("Hello Kotlin!") }
async: 像
launch
函数一样,它也用于启动一个新的协程;唯一的区别是它返回一个Deferred
而不是Job
。Deferred
是一个非阻塞的 future,承诺稍后提供结果。当生成的Deferred
被取消时,运行的协程也会被取消。async
构建器允许你通过调用await
来获取返回的值。fun main() = runBlocking<Unit> { val time = measureTimeMillis { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms") }
协程上下文
协程上下文是一组定义协程行为和特性的元素。它包括调度器、工作、异常处理器和协程名称等。上下文用于确定协程将如何以及在何处执行。
调度器
协程调度器负责确定协程将被执行的线程或线程。调度器有 4 种类型:
主线程调度器 (
Main Dispatchers
): 在主线程上执行协程。主线程调度器大部分在 UI 上工作。I/O 调度器 (
IO Dispatchers
): 在 I/O 线程上启动协程。此调度器使用按需创建的共享线程池。这适合可能阻塞执行线程的 I/O 操作,例如读取或写入文件、执行数据库查询或进行网络请求。默认调度器 (
Default Dispatchers
): 用于当作用域中没有明确指定其他调度器时。它利用共享后台线程池。这是计算密集型协程需要 CPU 资源的好选择。不受限调度器 (
Unconfined Dispatcher
): 允许协程在任何线程上运行,甚至在每次恢复时使用不同的线程。它适用于不消耗 CPU 或更新特定线程的共享数据的协程。
fun main() = runBlocking {
launch { // 父 runBlocking 协程的上下文
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // 不受限 -- 将使用主线程
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // 将被派发到 DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // 将获得自己的新线程
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
你也可以通过使用 Executor.asCoroutineDispatcher()
扩展函数将它们转换为 CoroutineDispatcher
来在任何你自己的线程池中执行协程。可以使用以下方法创建私有线程池:
newSingleThreadContext()
: 使用内置让步支持的专用线程构建协程执行环境。它是一个精细的 API,分配了本地资源(线程本身),需要仔细管理。newFixedThreadPoolContext()
: 建立一个固定大小的线程池的协程执行环境,可以在仔细管理线程资源的同时并行执行协程。
协程 Job
每次创建协程时,都会返回一个 Job
实例,以唯一标识该协程并允许你管理其生命周期。工作作为队列中的协程的句柄。一个工作有一组定义的状态:新建、活跃、完成中、已完成、取消中和已取消。我们不能直接访问这些状态本身,但我们可以访问工作的属性:isActive
、isCancelled
和 isCompleted
。
val job = launch { // 启动一个新协程并保留对其工作的引用
delay(1000L)
println("Hello World!")
}
job.join() // 等待子协程完成
println("Done")
SupervisorJob
它是 Job
的一个实现,作为子协程的监管者。它与常规工作类似,唯一的例外是它的子项可以相互独立地失败。子项的失败或取消不会导致监管者的工作失败或影响其其他子项,因此监管者可以为其子项的失败创建一个独特的处理策略。
fun main() = runBlocking {
val supervisorJob = SupervisorJob()
val coroutine1 = launch(supervisorJob) {
println("Coroutine 1")
throw RuntimeException("Error in Coroutine 1")
}
val coroutine2 = launch(supervisorJob) {
println("Coroutine 2")
delay(500)
println("Coroutine 2 completed")
}
coroutine1.join()
coroutine2.join()
println("Parent coroutine: ${supervisorJob.isActive}") // 输出: Parent coroutine: true
}
协程取消
协程的取消由 Job
管理(Job
是我们处理协程的句柄,它具有生命周期)。我们可以通过在其 Job
上调用 .cancel()
函数来取消协程。当启动多个协程时,我们可以依赖于取消协程被启动到的整个作用域,因为这将取消所有创建的子协程。
取消只不过是抛出一个 CancellationException
。这里的关键区别在于,如果协程抛出一个 CancellationException
,它被认为是正常取消的,而任何其他异常都被认为是失败。虽然来自协程库的挂起函数可以安全地取消,但在编写自己的代码时,应始终考虑与取消合作。
使代码可取消的一种方法是明确检查当前工作的状态。我们可以使用 isActive()
扩展函数在 CoroutineContext
和 CoroutineScope
上进行操作。检查取消的另一种常见方式是调用 ensureActive()
,这是 Job
、CoroutineContext
和 CoroutineScope
上可用的扩展函数。有关取消的更多细节可以在这里和这里找到。
感谢阅读!如果你学到了新东西,请关注我获取更多