Kotlin 协程

为什么要搞出和用协程呢

是节省CPU,避免系统内核级的线程频繁切换,造成的CPU资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。

是节约内存,在64位的Linux中,一个线程需要分配8MB栈内存和64MB堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。

是稳定性,前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。

是开发效率,使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时IO请求等。

对于协程的一个总结

特征:协程是运行在单线程中的并发程序

优点:省去了传统 Thread 多线程并发机制中切换线程时带来的线程上下文切换、线程状态切换、Thread 初始化上的性能损耗,能大幅度的提高并发性能

简单理解:在单线程上由程序员自己调度运行的并行计算

写到最后

协程本身不是替换线程的,因为协程是建立在线程之上的,但是协程能够更好的为我们提供执行高并发任务

1.kotlin中协程的特点:可以用同步的方式写出异步的代码

coroutineScope.launch(Dispatchers.Main){// 开始协程:主线程

    val token=api.getToken()// 网络请求:IO 线程

    val user=api.getUser(token)// 网络请求:IO 线程

    nameTv.text=user.name// 更新 UI:主线程

}

2.协程中挂起的本质

启动一个协程可以使用 launch 或者 async 函数,协程其实就是这两个函数中闭包的代码块。

launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。

3.协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。

那线程接下来会做什么呢?

如果它是一个后台线程:

要么无事可做,被系统回收

要么继续执行别的后台任务

跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。

如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务。

一个常见的场景是,获取一个图片,然后显示出来:

// 主线程中

GlobalScope.launch(Dispatchers.Main){

    valimage=suspendingGetImage(imageId)// 获取图片

    avatarIv.setImageBitmap(image)// 显示出来

}

suspend fun suspendingGetImage(id:String)=withContext(Dispatchers.IO){...}

协程:

线程的代码在到达suspend函数的时候被掐断,接下来协程会从这个suspend函数开始继续往下执行,不过是在指定的线程

谁指定的?是suspend函数指定的,比如我们这个例子中,函数内部的withContext传入的Dispatchers.IO所指定的 IO 线程。

Dispatchers调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行,关于Dispatchers这里先不展开了。

那我们平日里常用到的调度器有哪些?

常用的Dispatchers,有以下三种:

Dispatchers.Main:Android 中的主线程

Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求

Dispatchers.Default:适合 CPU 密集型的任务,比如计算

回到我们的协程,它从suspend函数开始脱离启动它的线程,继续执行在Dispatchers所指定的 IO 线程。

紧接着在suspend函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来

这个「切回来」是什么意思?

我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据Dispatchers切换到了 IO 线程;

当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再post一个Runnable,让我剩下的代码继续回到主线程去执行。

我们从线程和协程的两个角度都分析完成后,终于可以对协程的「挂起」suspend 做一个解释:

协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;

不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程

再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作

这个「切回来」的动作,在 Kotlin 里叫做 resume,恢复。

通过刚才的分析我们知道:挂起之后是需要恢复。

而恢复这个功能是协程的,如果你不在协程里面调用,恢复这个功能没法实现,所以也就回答了这个问题:为什么挂起函数必须在协程或者另一个挂起函数里被调用。

再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它其实直接或者间接地,总是会在一个协程里被调用的。

所以,要求suspend函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在suspend函数切换线程之后再切回

通过刚才的分析我们知道:挂起之后是需要恢复。

而恢复这个功能是协程的,如果你不在协程里面调用,恢复这个功能没法实现,所以也就回答了这个问题:为什么挂起函数必须在协程或者另一个挂起函数里被调用。

再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它其实直接或者间接地,总是会在一个协程里被调用的。

所以,要求suspend函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在 suspend 函数切换线程之后再切回来。

什么是「非阻塞式挂起」

非阻塞式是相对阻塞式而言的。

编程语言中的很多概念其实都来源于生活,就像脱口秀的段子一样。

线程阻塞很好理解,现实中的例子就是交通堵塞,它的核心有 3 点:

前面有障碍物,你过不去(线程卡了)

需要等障碍物清除后才能过去(耗时任务结束)

除非你绕道而行(切到别的线程)

从语义上理解「非阻塞式挂起」,讲的是「非阻塞式」这个是挂起的一个特点,也就是说,协程的挂起,就是非阻塞式的,协程是不讲「阻塞式的挂起」的概念的。

我们讲「非阻塞式挂起」,其实它有几个前提:并没有限定在一个线程里说这件事,因为挂起这件事,本来就是涉及到多线程。

就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。

所以「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。

为什么要讲非阻塞式挂起

「非阻塞式挂起」和第二篇的「挂起要切线程」是同一件事情,那还有讲的必要吗?

是有的。因为它在写法上和单线程的阻塞式是一样的。

协程只是在写法上「看起来阻塞」,其实是「非阻塞」的,因为在协程里面它做了很多工作,其中有一个就是帮我们切线程。

之前说的挂起,重点是说切线程先切过去,然后再切回来。

而这里的非阻塞式,重点是说线程虽然会切,但写法上和普通的单线程差不多。

让我们来看看下面的例子:

main{

GlobalScope.launch(Dispatchers.Main){// 耗时操作val user=suspendingRequestUser()

updateView(user)

}

private suspend fun suspendingRequestUser():User=withContext(Dispatchers.IO){api.requestUser()}}

阻塞的本质

首先,所有的代码本质上都是阻塞式的,而只有比较耗时的代码才会导致人类可感知的等待,比如在主线程上做一个耗时 50 ms 的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。

举个例子,当你开发的 app 在性能好的手机上很流畅,在性能差的老手机上会卡顿,就是在说同一行代码执行的时间不一样。

视频中讲了一个网络 IO 的例子,IO 阻塞更多是反映在「等」这件事情上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。

而这跟协程半毛钱关系没有,切线程解决不了的事情,协程也解决不了

总结

关于这边文章的标题协程是什么、挂起是什么、挂起的非阻塞式可以做下面的总结

协程是什么

协程就是切线程;

挂起是什么

挂起就是可以自动切回来的切线程;

挂起的非阻塞式

挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作。

2.在kotlin使用协程

项目中配置对 Kotlin 协程的支持

在使用协程之前,我们需要在 build.gradle 文件中增加对 Kotlin 协程的依赖:

项目根目录下的 build.gradle :

buildscript {

    ext.kotlin_coroutines = '1.4.0'

}

Module 下的 build.gradle

dependencies {

        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'

}

创建协程

kotlin 中 GlobalScope 类提供了几个携程构造函数:

launch - 创建协程

async - 创建带返回值的协程,返回的是 Deferred 类

withContext - 不创建新的协程,指定协程上运行代码块

runBlocking - 不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会

先跑起来一个简单的例子:

import kotlinx.coroutines.*

fun main(){

        GlobalScope.launch{

        // 在后台启动一个新的协程并继续

        delay(1000L)// 非阻塞的等待 1 秒钟(默认时间单是毫秒)

        println("World!")// 在延迟后打印输出}

        println("Hello,")// 协程已在等待时主线程还在继续

        Thread.sleep(2000L)// 阻塞主线程 2 秒钟来保证 JVM 存活

}

协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。那么可以将上面的代码写成这样:

coroutineScope.launch(

    Dispatchers.Main){//  在 UI 线程开始

    val image=withContext(Dispatchers.IO){// 切换到 IO 线程,并在执行完成后切回 UI 线程

    getImage(imageId)// 将会运行在 IO 线程}

    avatarIv.setImageBitmap(image)// 回到 UI 线程更新 UI

}

我们甚至可以把 withContext 放进一个单独的函数里面:

launch(Dispatchers.Main){//  在 UI 线程开始

val image=getImage(imageId)

avatarIv.setImageBitmap(image)//  执行结束后,自动切换回 UI 线程}//

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


launch 函数

launch 函数定义

public fun CoroutineScope.launch(

    context:CoroutineContext=EmptyCoroutineContext,

    start:CoroutineStart=CoroutineStart.DEFAULT,

    block:suspendCoroutineScope.()->Unit):Job

launch 是个扩展函数,接受3个参数,前面2个是常规参数,最后一个是个对象式函数,这样的话 kotlin 就可以使用以前说的闭包的写法:() 里面写常规参数,{} 里面写函数式对象的实现,就像上面的例子一样

我们需要关心的是 launch 的3个参数和返回值 Job:

CoroutineContext - 可以理解为协程的上下文,在这里我们可以设置 CoroutineDispatcher 协程运行的线程调度器,有 4种线程模式:

Dispatchers.Default

Dispatchers.IO -

Dispatchers.Main - 主线程

Dispatchers.Unconfined - 没指定,就是在当前线程

不写的话就是 Dispatchers.Default 模式的,或者我们可以自己创建协程上下文,也就是线程池,newSingleThreadContext 单线程,newFixedThreadPoolContext 线程池

CoroutineStart - 启动模式,默认是DEAFAULT,也就是创建就启动;还有一个是LAZY,意思是等你需要它的时候,再调用启动

DEAFAULT - 模式模式,不写就是默认

ATOMIC -

UNDISPATCHED

LAZY - 懒加载模式,你需要它的时候,再调用启动

block - 闭包方法体,定义协程内需要执行的操作

Job - 协程构建函数的返回值,可以把 Job 看成协程对象本身,协程的操作方法都在 Job 身上了

job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动

job.join() - 等待协程执行完毕

job.cancel() - 取消一个协程

job.cancelAndJoin() - 等待协程执行完毕然后再取消

创建一个协程

创建该launch函数返回了一个可以被用来取消运行中的协程的Job

val job=launch{

    repeat(1000){i->

        println("job: I'm sleeping$i...")

        delay(500L)

    }

}

取消

val  job=launch{

repeat(1000){

i->println("job: I'm sleeping $i ...")

delay(500L)}}

delay(1300L)// 延迟一段时间

println("main: I'm tired of waiting!")job.cancel()// 取消该作业

job.join()// 等待作业执行结束

println("main: Now I can quit.")

程序执行后的输出如下:

job: I'm sleeping 0 ...

job: I'm sleeping 1 ...

job: I'm sleeping 2 ...

main: I'm tired of waiting!

main: Now I can quit.

一旦 main 函数调用了 job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了

超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。 来看看示例代码:

withTimeout(1300L){repeat(1000){i->

println("I'm sleeping $i ...")

delay(500L)}}

运行后得到如下输出:

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

这里我们看到了TimeoutCancellationException异常,这异常是因为超时导致的异常取消

解决这个问题也很简单通过withTimeout的withTimeoutOrNull函数,代码示例:

valresult=withTimeoutOrNull(1300L){repeat(1000){i->println("I'm sleeping$i...")delay(500L)}"Done"// 在它运行得到结果之前取消它}println("Result is$result")

运行后得到如下输出:

I'm sleeping 0 ...

I'm sleeping 1 ...

I'm sleeping 2 ...

Result is null

这样就没有抛出异常了


async 函数定义

public fun CoroutineScope.async(

    context:CoroutineContext=EmptyCoroutineContext,

    start:CoroutineStart=CoroutineStart.DEFAULT,

    block:suspendCoroutineScope.()->T):Deferred{

        val  newContext=newCoroutineContext(context)

        val coroutine=if(start.isLazy){

        LazyDeferredCoroutine(newContext,block)

}else{DeferredCoroutine(newContext,active=true)

        coroutine.start(start,coroutine,block)

        return coroutine

}

从源码可以看出launch 和 async的唯一区别在于async的返回值

async 返回的是 Deferred 类型,Deferred 继承自 Job 接口,Job有的它都有,增加了一个方法 await ,这个方法接收的是 async 闭包中返回的值,async 的特点是不会阻塞当前线程,但会阻塞所在协程,也就是挂起

但是需要注意的是async 并不会阻塞线程,只是阻塞锁调用的协程

async和launch的区别

launch 更多是用来发起一个无需结果的耗时任务,这个工作不需要返回结果。

async 函数则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值。

runBlocking

runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。

runBlocking 通常适用于单元测试的场景,而业务开发中不会用到这个函数

relay、yield

relay 和 yield 方法是协程内部的操作,可以挂起协程,

relay、yield 的区别

relay 是挂起协程并经过执行时间恢复协程,当线程空闲时就会运行协程

yield 是挂起协程,让协程放弃本次 cpu 执行机会让给别的协程,当线程空闲时再次运行协程。

我们只要使用 kotlin 提供的协程上下文类型,线程池是有多个线程的,再次执行的机会很快就会有的。

除了 main 类型,协程在挂起后都会封装成任务放到协程默认线程池的任务队列里去,有延迟时间的在时间过后会放到队列里去,没有延迟时间的直接放到队列里去


原文链接:https://www.jianshu.com/p/402a69dbd66d

你可能感兴趣的:(Kotlin 协程)