前言
kotlin的协程在初学者看来是一个很神奇的东西,居然能做到用同步的代码块实现异步的调用,其实深入了解你会发现kotlin协程本质上是通过函数式编程的风格对Java线程池的一种封装,这样会带来很多好处,首先是函数式+响应式编程风格避免了回调地狱,这也可以说是实现promise,future等语言(比如js)的进一步演进。其次是能够避免开发者的失误导致的线程切换过多的性能损失。
那么我们就来看看协程
一、协程(Coroutines)是什么
1、协程是轻量级线程
- 协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。
- 协程就是方法调用封装成类线程的API。方法调用当然比线程切换轻量;而封装成类线程的API后,它形似线程(可手动启动、有各种运行状态、能够协作工作、能够并发执行)。因此从这个角度说,它是轻量级线程没错;
- 当然,协程绝不仅仅是方法调用,因为方法调用不能在一个方法执行到一半时挂起,之后又在原点恢复。这一点可以使用EventLoop之类的方式实现。想象一下在库级别将回调风格或Promise/Future风格的异步代码封装成同步风格,封装的结果就非常接近协程;
2、线程运行在内核态,协程运行在用户态
主要明白什么叫用户态,我们写的几乎所有代码,都执行在用户态,协程对于操作系统来说仅仅是第三方提供的库而已,当然运行在用户态。而线程是操作系统级别的东西,运行在内核态。
3、协程是一个线程框架
Kotlin的协程库可以指定协程运行的线程池,我们只需要操作协程,必要的线程切换操作交给库,从这个角度来说,协程就是一个线程框架
4、协程实现
协程,顾名思义,就是相互协作的子程序,多个子程序之间通过一定的机制相互关联、协作地完成某项任务。比如一个协程在执行上可以被分为多个子程序,每个子程序执行完成后主动挂起,等待合适的时机再恢复;一个协程被挂起时,线程可以执行其它子程序,从而达到线程高利用率的多任务处理目的——协程在一个线程上执行多个任务,而传统线程只能执行一个任务,从多任务执行的角度,协程自然比线程轻量;
5、协程解决的问题
同步的方式写异步代码。如果不使用协程,我们目前能够使用的API形式主要有三种:纯回调风格(如AIO)、RxJava、Promise/Future风格,他们普遍存在回调地狱问题,解回调地狱只能通过行数换层数,且对于不熟悉异步风格的程序员来说,能够看懂较为复杂的异步代码就比较费劲。
6、协程优点
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发;
二、协程使用
- 依赖
- dependencies {
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
- }
协程需要运行在协程上下文环境,在非协程环境中凭空启动协程,有三种方式
1、runBlocking{}
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- Log.e(TAG, "主线程id:${mainLooper.thread.id}")
- test()
- Log.e(TAG, "协程执行结束")
- }
- private fun test() = runBlocking {
- repeat(8) {
- Log.e(TAG, "协程执行$it 线程id:${Thread.currentThread().id}")
- delay(1000)
- }
- }
runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。
2、GlobalScope.launch{}
在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。
由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- Log.e(TAG, "主线程id:${mainLooper.thread.id}")
- val job = GlobalScope.launch {
- delay(6000)
- Log.e(TAG, "协程执行结束 -- 线程id:${Thread.currentThread().id}")
- }
- Log.e(TAG, "主线程执行结束")
- }
- //Job中的方法
- job.isActive
- job.isCancelled
- job.isCompleted
- job.cancel()
- jon.join()
从执行结果看出,launch不会阻断主线程。
下面我们来总结launch
我们看一下launch方法的定义:
- public fun CoroutineScope.launch(
- context: CoroutineContext = EmptyCoroutineContext,
- start: CoroutineStart = CoroutineStart.DEFAULT,
- block: suspend CoroutineScope.() -> Unit
- ): Job {
- val newContext = newCoroutineContext(context)
- val coroutine = if (start.isLazy)
- LazyStandaloneCoroutine(newContext, block) else
- StandaloneCoroutine(newContext, active = true)
- coroutine.start(start, coroutine, block)
- return coroutine
- }
从方法定义中可以看出,launch() 是CoroutineScope的一个扩展函数,CoroutineScope简单来说就是协程的作用范围。launch方法有三个参数:1.协程下上文;2.协程启动模式;3.协程体:block是一个带接收者的函数字面量,接收者是CoroutineScope
①.协程下上文
- 上下文可以有很多作用,包括携带参数,拦截协程执行等等,多数情况下我们不需要自己去实现上下文,只需要使用现成的就好。上下文有一个重要的作用就是线程切换,Kotlin协程使用调度器来确定哪些线程用于协程执行,Kotlin提供了调度器给我们使用:
- Dispatchers.Main:使用这个调度器在 Android 主线程上运行一个协程。可以用来更新UI 。在UI线程中执行
- Dispatchers.IO:这个调度器被优化在主线程之外执行磁盘或网络 I/O。在线程池中执行
- Dispatchers.Default:这个调度器经过优化,可以在主线程之外执行 cpu 密集型的工作。例如对列表进行排序和解析 JSON。在线程池中执行。
- Dispatchers.Unconfined:在调用的线程直接执行。
- 调度器实现了CoroutineContext接口。
②.启动模式
在Kotlin协程当中,启动模式定义在一个枚举类中:
- public enum class CoroutineStart {
- DEFAULT,
- LAZY,
- @ExperimentalCoroutinesApi
- ATOMIC,
- @ExperimentalCoroutinesApi
- UNDISPATCHED;
- }
一共定义了4种启动模式,下表是含义介绍:
- DEFAULT:默认的模式,立即执行协程体
- LAZY:只有在需要的情况下运行
- ATOMIC:立即执行协程体,但在开始运行之前无法取消
- UNDISPATCHED:立即在当前线程执行协程体,直到第一个 suspend 调用
③.协程体
协程体是一个用suspend关键字修饰的一个无参,无返回值的函数类型。被suspend修饰的函数称为挂起函数,与之对应的是关键字resume(恢复),注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。
suspend函数会将整个协程挂起,而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行的。看下面的代码示例:
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- GlobalScope.launch {
- val token = getToken()
- val userInfo = getUserInfo(token)
- setUserInfo(userInfo)
- }
- repeat(8){
- Log.e(TAG,"主线程执行$it")
- }
- }
- private fun setUserInfo(userInfo: String) {
- Log.e(TAG, userInfo)
- }
- private suspend fun getToken(): String {
- delay(2000)
- return "token"
- }
- private suspend fun getUserInfo(token: String): String {
- delay(2000)
- return "$token - userInfo"
- }
getToken方法将协程挂起,协程中其后面的代码永远不会执行,只有等到getToken挂起结束恢复后才会执行。同时协程挂起后不会阻塞其他线程的执行。
3.async/await:Deferred
async跟launch的用法基本一样,区别在于:async的返回值是Deferred,将最后一个封装成了该对象。async可以支持并发,此时一般都跟await一起使用,看下面的例子。
async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的;
async用于启动一个异步的协程任务,await用于去得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- GlobalScope.launch {
- val result1 = GlobalScope.async {
- getResult1()
- }
- val result2 = GlobalScope.async {
- getResult2()
- }
- val result = result1.await() + result2.await()
- Log.e(TAG,"result = $result")
- }
- }
- private suspend fun getResult1(): Int {
- delay(3000)
- return 1
- }
- private suspend fun getResult2(): Int {
- delay(4000)
- return 2
- }
async是不阻塞线程的,也就是说getResult1和getResult2是同时进行的,所以获取到result的时间是4s,而不是7s。
三、协程异常
1、因协程取消,协程内部suspend方法抛出的CancellationException
2、常规异常,这类异常,有两种异常传播机制
- launch:将异常自动向父协程抛出,将会导致父协程退出
- async: 将异常暴露给用户(通过捕获deffer.await()抛出的异常)
例子讲解
- 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
总结:
- 协程是可以被取消的和超时控制,可以组合被挂起的函数,协程中运行环境的指定,也就是线程的切换
- 协程最常用的功能是并发,而并发的典型场景就是多线程。
- 协程设计的初衷是为了解决并发问题,让协作式多任务实现起来更加方便。
- 简单理解 Kotlin 协程的话,就是封装好的线程池,也可以理解成一个线程框架。
本文转载自微信公众号「 Android开发编程」,可以通过以下二维码关注。转载本文请联系 Android开发编程众号。