为什么要搞出和用协程呢
是节省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
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
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