一起掌握Kotlin协程基础
前言
在平时的开发中,我们经常会跟线程打交道,当执行耗时操作时,便需要开启线程去执行,防止主线程阻塞。但是开启太多的线程,它们的切换就需要耗费很多的资源,并且难以去控制,没有及时停止或者控制不当便很可能会造成内存泄露。并且在开启多线程后,为了能够获取到计算结果,我们需要采用回调的方式来回调结果,但是回调多了,代码的可读性变得很差。kotlin协程是运行在线程之上,我们使用它时能够很好地去控制它,并且在切换方面,它消耗的CPU和内存大大地降低,它不会阻塞所在线程,可以在不用使用回调的情况下便可以直接获取计算结果。
正文
协程程序
GlobalScope.launch { // 在后台启动一个新的协程并继续
println("hello Coroutine")
}
//输出:hello Coroutine
GlobalScope调用launch会开启一个协程。
协程的组成
- CoroutineScope
- CoroutineContext:可指定名称、Job(管理生命周期)、指定线程(Dispatchers.Default适合CUP密集型任务、Dispatchers.IO适合磁盘或网络的IO操作、Dispatchers.Main用于主线程)
- 启动:launch(启动协程,返回一个job)、async(启动带返回结果的协程)、withContext(启动协程,可出阿如上下文改变协程的上下文)
作业
当我们开启协程后,可能需要对开启的协程进行控制,比如在不再需要该协程的返回结果时,可将其进行取消。好在调用launch函数后,会返回一个协程的job,我们可利用这个job来进行取消操作。
val job = GlobalScope.launch {
delay(1000)
println("world")
}
println("hello")
job.cancel()
//输出结果为:hello
可能有的同学会觉得奇怪为什么world没有输出,原因是当调用cancel时,会对该协程进程取消,也就是不再执行了直接停止。下面再看一个join方法:
val job = GlobalScope.launch {
delay(1000)
println("world")
}
println("hello")
job.join()
//输出结果:
//hello
//world
join方法会等待该协程执行结束。
超时
当一个协程执行超时,我们可能需要取消它,但手动跟踪它的超时可能会觉得麻烦,所以我们可以使用withTimeout方法来进程超时跟踪:
withTimeout(1300) {
repeat(10){i->
println("i-->$i")
delay(500)
}
}
//i-->0
//i-->1
//i-->2
//抛出TimeoutCancellationException异常
这个方法在设置的超时时间还没完成时,抛出TimeoutCancellationException异常。如果我们只是单纯防止超时而不抛出异常,则可使用:
val wton = withTimeoutOrNull(1300){
repeat(10){i->
println("i-->$i")
delay(500)
}
}
println("end -- $wton")
//i-->0
//i-->1
//i-->2
//end -- null
挂起函数
当我们在launch函数中写了很多代码,这看上去并不美观,为了可以抽取出逻辑放到一个单独的函数中,我们可以使用suspend 修饰符来修饰一个方法,这样的函数为挂起函数:
suspend fun doCalOne():Int{
delay(1000)
return 5
}
挂起函数需要在挂起函数或者协程中调用,普通方法不能调用挂起函数。
我们通过使用两个挂起函数来获取它们各自的计算结果,然后对获取的结果进一步操作:
suspend fun doCalOne():Int{
delay(1000)
return 5
}
suspend fun doCalTwo():Int{
delay(1500)
return 3
}
coroutineScope {
val time = measureTimeMillis {
//同步开始,需要按顺序等待
val one = doCalOne()
val two = doCalTwo()
println("one + two = ${one + two}")
}
println("time is $time")
}
//one + two = 8
//time is 2512
我们可以看到,计算结果正确,说明能够正常返回,而且总共的耗时是跟两个方法所用的时间的总和(忽略其他),那我们有没有办法让两个计算方法并行运行能,答案是肯定的,我们只需使用async便可以实现:
coroutineScope {
val time = measureTimeMillis {
//异步开始
val one = async{doCalOne()}
val two = async{doCalTwo()}
//同步开始,需要按顺序等待
println("one + two = ${one.await() + two.await()}")
}
println("time is $time")
}
//one + two = 8
//time is 1519
我们可以看到,计算结果正确,并且所需时间大大减少,接近运行最长的计算函数。
async类似于launch函数,它会启动一个单独的协程,并且可以与其他协程并行。它返回的是一个Deferred(非阻塞式的feature),当我们调用await方法才可以得到返回的结果。
async有多种启动方式,下面实例为懒性启动:
coroutineScope {
//调用await或者start协程才被启动
val one = async(start = CoroutineStart.LAZY){doCalOne()}
val two = async(start = CoroutineStart.LAZY){doCalTwo()}
one.start()
two.start()
}
我们可以调用start或者await来启动它。
结构化并发
虽然协程很轻量,但它运行时还是需要耗费一些资源,如果我们在使用的过程中,忘记对它进行引用,并且及时地停止它,那将会造成资源浪费或者出现内存泄露等问题。但是一个一个跟踪(也就是使用返回的job)很不方便,一个两个还好管理,但是多了却不方便管理。于是我们可以使用结构化并发,这样我们可以在指定的作用域中启动协程。这点跟线程的区别在于线程总是全局的。大致如图(图片):
在日常开发中,我们会经常开启网络请求,有时候需要同时发起多个网络请求,我们想要的是在挂起函数中启动多个请求,当挂起函数返回时,里边的请求都执行结束,那么我们可以使用coroutineScope 来进行指定一个作用域:
suspend fun twoFetch(){
coroutineScope {
launch {
delay(1000L)
doNetworkJob("url--1")
}
launch { doNetworkJob("url--2") }
}
}
fun doNetworkJob(url : String){
println(url)
}
//url--2
//url--1
coroutineScope等到在其里边开启的所有协程执行完成再返回。所以twoFetch不会在coroutineScope内部所启动的协程完成前返回。
当我们取消协程时,会通过层次结构来进行传递的。
suspend fun errCoroutineFun(){
coroutineScope {
try {
failedCorou()
}catch (e : RuntimeException) {
println("fail with RuntimeException")
}
}
}
suspend fun failedCorou() {
coroutineScope {
launch {
try {
delay(Long.MAX_VALUE)
println("after delay")
} finally {
println("one finally")
}
}
launch {
println("two throw execption")
throw RuntimeException("")
}
}
}
//two throw execption
//one finally
//fail with RuntimeException
结语
本次的kotlin协程分享也结束了,内容篇基础,也算是对kotlin协程的一个入门。当对它的使用达到熟练时,会继续分享一篇关于较进阶的文章,希望大家喜欢。
链接:https://juejin.cn/post/7127086841685098503
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。