zwvista

导航

Kotlin语言学习笔记(8)协程

Coroutine(协程)

为了避免在主线程中执行过多任务导致 UI 反应迟钝,不可避免的必须使用多线程。
但由于多线程耗费太多资源,Kotlin 语言引入了协程以替代线程。
Kotlin 语言中的协程可以被想象成一种轻量级的线程。可以轻易开启百万级别的协程而不必担心资源耗费过大。
与其他语言(C#,Go)不同,Kotlin 语言中的协程大致上是通过协程库来实现的,语言级别的支持非常有限。

  • 协程在线程中被执行。
  • 协程可以被挂起被恢复。
  • 协程可以切换上下文(所处的线程等)。

Scope(作用域)

为了便于管理协程,协程库引入了协程作用域。
协程作用域是协程的具体表现形式,协程在协程作用域内创建并执行。
协程作用域的类型是CoroutineScope

协程作用域

  • GlobalScope(全局作用域)
  • CoroutineScope(普通作用域)
  • lifecycleScope(生命周期作用域,仅限于Android)
  • viewModelScope(ViewModel作用域,仅限于Android)
  • liveDataModelScope(LiveData作用域,仅限于Android)

Suspend(挂起)

suspend 是 Kotlin 语言为协程提供的唯一关键字。
带 suspend 的函数被称为 suspending function(挂起函数),挂起函数只能在协程作用域中被调用。
普通函数不能直接调用挂起函数,普通函数必须创建协程作用域才能在其中调用挂起函数。
挂起函数可以直接调用别的挂起函数。

Builder(构建器)

协程作用域必须通过协程构建器来构造,
协程构建器创建一个新的协程作用域并在此协程作用域内创建并启动一个新的协程。
协程构建器也是一个协程作用域构建器。
协程构建器可以在当前(外部)协程作用域之内创建一个新的(内部)协程作用域,形成协程作用域的嵌套。
协程构建器函数返回一个标志协程本身的Job (作业)对象,可以认为协程的类型是Job。

  • launchasync(相同点)
    launch 和 async 都是挂起函数。
    launch 和 async 都是协程作用域类的扩展方法。
    launch 和 async 方法均带有一个尾部闭包参数,用于指定协程的具体操作。
    在协程作用域内依次调用多个挂起函数时,这些挂起函数是顺序执行的。
    要并发执行多个挂起函数,必须使用 launch 和 async 启动多个协程,分别执行这些挂起函数。
  • launchasync(不同点)
    launch 方法的尾部闭包参数不返回值,launch 方法本身返回一个 Job (作业)对象
    async 方法的尾部闭包参数返回一个 T 类型的值,async 方法本身返回一个 Deferred<T> (延迟)对象
    这里的作业对象和延迟对象可以理解为其他语言中的 Future,Promise
    即 launch 用于启动不需要返回值的协程,而 async 用于启动需要返回值的协程。
  • runBlocking
    runBlocking 是一个全局函数,用于桥接阻塞世界(blocking world 指线程)和非阻塞世界(non-blocking world 指协程)
    runBlocking 阻塞当前线程,通常仅用于 main 函数和测试函数中。
  • coroutineScope
    coroutineScope 是一个协程作用域构建器。
    coroutineScope 是一个挂起函数,必须在协程内调用。
  • withContext
    withContext 是一个挂起函数,必须在协程内调用。
    withContext 通常用于切换协程上下文。

async 和 await

使用 async 启动新的协程并执行挂起函数后,async 函数会返回带返回值的 Deferred 对象。
通过调用 Deferred 对象的 await 方法可以得到协程真正的返回值。
async 和 await 在 Kotlin 语言中都不是关键字
协程构建器 async 返回 Deferred 对象,Deferred 对象也是一种 Job 对象。
这里 Deferred 和 Job 都是接口(interface), Deferred 接口继承自 Job 接口。
withContext 相当于 async + await

Dispatcher(调度器)

协程所处的线程由协程调度器来管理。
在调用协程构建器(launch, async, runBlocking)时可以指定协程调度器。
协程调度器有

  • Dispatchers.Default
    在缺省线程池所分配的线程中启动协程,通常用于繁重的计算任务。
  • Dispatchers.IO
    在 IO 线程中启动协程,通常用于调用网络服务,读写文件等 IO 操作。
  • Dispatchers.Unconfined
    在当前线程空闲时启动协程
  • Dispatchers.Main
    在主线程中启动协程,仅限于Android

如果需要在当前协程内切换线程,可以使用 withContext 函数并在参数中指定新的调度器。

Context(上下文)

协程通过上下文得以标识。
协程作用域本质上是协程上下文的引用。
协程的上下文包含以下信息:

  • 协程的名字
  • 协程所处的线程的名字
  • 协程所创建的Job对象
  • 协程所使用的调度器

协程上下文的类型是CoroutineContext
launch 和 async 之类的协程构建器函数带有一个可选的协程上下文参数,可以在创建协程时指定协程上下文。

除了使用 IDE 以外还可以
调用 coroutineContext 来读取协程上下文
调用 Thread.currentThread().name 来读取协程所处的线程的名字

Coroutine Context and Scope

delay 和 sleep(等待)

在线程中要等待(延迟)一段时间需要使用 Thread.sleep()
在协程中要等待(延迟)一段时间需要使用 delay()
delay 是一个挂起函数

Cancellation(取消)

通过调用 Job(作业)对象的 cancel 方法可以取消并中止协程的执行。
作业对象可以被调用的方法有

  • cancel 方法
    取消协程的执行
  • join 方法
    等待协程执行完毕
  • cancelAndJoin 方法
    取消并等待协程执行完毕

Cooperative(协作性)

协程要能被取消,协程本身必须具有协作性。
如果协程本身不具有协作性,协程就不能被取消,也就是只有等待协程自身执行完毕。
所有挂起函数都具有协作性。
调用协程作用域的 isActive 属性也可以让协程具有协作性。

Timeout(超时)

withTimeout 函数可以在协程内部对某些操作限时,若超时协程会自动抛出 TimeoutCancellationException 异常。

Structured concurrency(结构化并发)

GlobalScope.launch 可用于创建全局作用域内的协程,这类协程的寿命和应用程序一样长。
主线程执行完毕时全局作用域内的协程将被取消。
为了让主线程等待全局作用域内的协程执行完毕,必须显式调用这类协程在创建时所返回的 Job 对象的 join 方法。

runBlocking 可用于创建 CoroutineScope(普通作用域)内的协程。
在普通作用域内的(外部)协程内通过调用 launch async coroutineScope 等协程作用域构建器可以创建新的(内部)协程作用域,形成协程作用域的嵌套。
普通作用域内的协程作用域嵌套被称为结构化并发:
外部协程会自动 join(联结)内部协程,即外部协程即便执行完毕了也必须等到内部协程全部执行完毕并释放之后才能被释放。
外部协程在被取消时会自动取消所有内部协程。
结构化并发有助于防止或减少资源泄漏。(协程虽所耗资源不多,但是不关闭的话同样可能引发内存以及其他所使用的资源的泄漏)

import kotlinx.coroutines.*
fun main() = runBlocking {
    val deferred: Deferred<Int> = async(Dispatchers.Default) {
        loadData()
    }
    println("waiting...")
    println(deferred.await())
}
suspend fun loadData(): Int {
    println("loading...")
    delay(1000L)
    println("loaded!")
    return 42
}
/*
waiting...
loading...
loaded!
42
*/

Channels(信道)

信道是一种在协程之间传递数据的机制。

Flow(异步流)

posted on 2021-03-01 09:53  zwvista  阅读(329)  评论(0编辑  收藏  举报