Kotlin-Coroutines

协程-基础2

2021-02-02  本文已影响0人  有腹肌的豌豆Z

概述

解释协程

1.协程是轻量级线程(官方表述)
可以换个说法,协程就是方法调用封装成类线程的API。方法调用当然比线程切换轻量;而封装成类线程的API后,它形似线程(可手动启动、有各种运行状态、能够协作工作、能够并发执行)。因此从这个角度说,它是轻量级线程没错。
当然,协程绝不仅仅是方法调用,因为方法调用不能在一个方法执行到一半时挂起,之后又在原点恢复。这一点可以使用EventLoop之类的方式实现。想象一下在库级别将回调风格或Promise/Future风格的异步代码封装成同步风格,封装的结果就非常接近协程了。
而协程和线程之间的区别,往大了说,那就是普通函数与线程的区别;往小了说,就是EventLoop和线程的区别。他们之间的唯一的关系,仅仅在于协程的代码是运行在线程中。一个不恰当的类比,人和地球(地球提供生成环境,人在其中生存)

  1. 线程运行在内核态,协程运行在用户态
    主要明白什么叫用户态,我们写的几乎所有代码,都执行在用户态,协程对于操作系统来说仅仅是第三方提供的库而已,当然运行在用户态。而线程是操作系统级别的东西,运行在内核态。

  2. 协程是一个线程框架(扔物线表述)
    对某些语言,比如Kotlin,这样说是没有问题的,Kotlin的协程库可以指定协程运行的线程池,我们只需要操作协程,必要的线程切换操作交给库,从这个角度来说,协程就是一个线程框架。
    但理论上我们可以在单线程语言如JavaScript、Python上实现协程(事实上他们已经实现了协程),这时我们再叫它线程框架可能就不合适了。

使用协程

启动

协程需要运行在协程上下文环境,在非协程环境中凭空启动协程,有三种方式

在一个协程中启动子协程,一般来说有两种方式

取消

launch{}返回Job,async{}返回Deffer,Job和Deffer都有cancel()方法,用于取消协程。

从协程内部看取消的效果

上面两个特性和线程的interrupt机制非常类似,理解起来并不难。

val job = launch {
    // 如果这里不检测isActive标记,协程就不会被正常cancel,而是执行直到正常结束
    while (isActive) { 
        ......
    }
}
job.cancelAndJoin() // 取消该作业并等待它结束

了解协程的启动和取消,对于最基本的使用已经足够了。不过为了更加安全放心地使用,需要更加深入地了解,我们从核心组件说起。

异常

Kotlin协程的异常有两种

这里借用官方例子讲解

fun main() = runBlocking {
    val job = GlobalScope.launch { // root coroutine with launch
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // root coroutine with async
        println("Throwing exception from async")
        throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}

输出结果

Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException

注意,例子是在GlobalScope.launch{}中抛异常,不会导致父协程退出。GlobalScope 是全局的生命周期伴随着整个程序。

核心组件

协程上下文

顾名思义,协程上下文表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等。通过CoroutineContext定义,CoroutineContext被定义为一个带索引的集合,集合的元素为Element,上面所提到调度器、Job等都实现了Eelement接口。
由于CoroutineContext被定义为集合,因此在实际使用时可以自由组合加减各种上下文元素。
启动子协程时,子协程默认会继承除Job外的所有父协程上下文元素,创建新的Job,并将父Job设置为当前Job的父亲。
启动子协程时,可以指定协程上下文元素,如果父上下文中存在该元素则覆盖,不存在则添加。

调度器

调度器是协程上下文中众多元素中最重要的一个,通过CoroutineDispatcher定义,它控制了协程以何种策略分配到哪些线程上运行。这里介绍几种常见的调度器

注意,由于上下文具有继承关系,因此启动子协程时不显式指定调度器时,子协程和父协程是使用相同调度器的。

Job

Job也是上下文元素,它代表协程本身。Job能够被组织成父子层次结构,并具有如下重要特性。

类似Thread,一个Job可能存在多种状态


我们直接使用launch获取到的job已经处于Active装填,启动时加上LAZY参数时则得到New状态的Active。
各状态转换关系如下,注意,Completing只是一个内部状态,外部观察还是Active状态。

要区分是主动取消还是异常导致一个协程退出,可以getCancellationException()查看退出原因。

作用域

协程作用域——CoroutineScope,用于管理协程,管理的内容有

区分作用域和上下文

从类定义看,CoroutineScope和CoroutineContext非常类似,最终目的都是协程上下文,但正如Kotlin协程负责人Roman Elizarov在Coroutine Context and Scope中所说,二者的区别只在于使用目的的不同——作用域用于管理协程;而上下文只是一个记录协程运行环境的集合。他们的关系如下。

约定和经验

避免使用GlobalScope.launch

GlobalScope是实现了CoroutineScope的单例对象,含有一个空的上下文对象

// GlobalScope的定义
public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

这意味着它的生命周期与整个应用绑定,并且永远不会被主动取消。这样启动的协程只有两个归宿:

这是危险的。考虑极端情况:

在一个实例方法中使用GlobalScope.launch启动了一个CPU密集型协程,且执行时间较长
在启动协程后,该实例方法因异常退出,所属对象也被销毁
反复多次出现步骤1\2

这样导致的结果是启动了超多CPU密集型任务,最终导致应用卡顿,甚至资源耗尽。

解决方案是避免使用GlobalScope。正确的做法是将自己的组件实现CoroutineScope,并在组件销毁时调用作用域的cancel()方法。实现方式多使用委托。

// 官方例子
class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onDestroy() {
         cancel() // cancel is extension on CoroutineScope
    }
    ... ...
}
// vertx例子
abstract class CoroutineVerticle : Verticle, CoroutineScope {
  // 默认上下文使用context.dispatcher()
  override val coroutineContext: CoroutineContext by lazy { context.dispatcher() }
  ... ...
}

区分与对比

Kotlin中,有几种方式能够启动协程,或者看似能够启动协程,这里列举

上一篇 下一篇

猜你喜欢

热点阅读