一起掌握Kotlin协程基础

1,205 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

前言

在平时的开发中,我们经常会跟线程打交道,当执行耗时操作时,便需要开启线程去执行,防止主线程阻塞。但是开启太多的线程,它们的切换就需要耗费很多的资源,并且难以去控制,没有及时停止或者控制不当便很可能会造成内存泄露。并且在开启多线程后,为了能够获取到计算结果,我们需要采用回调的方式来回调结果,但是回调多了,代码的可读性变得很差。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)很不方便,一个两个还好管理,但是多了却不方便管理。于是我们可以使用结构化并发,这样我们可以在指定的作用域中启动协程。这点跟线程的区别在于线程总是全局的。大致如图(图片):

image.png

在日常开发中,我们会经常开启网络请求,有时候需要同时发起多个网络请求,我们想要的是在挂起函数中启动多个请求,当挂起函数返回时,里边的请求都执行结束,那么我们可以使用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协程的一个入门。当对它的使用达到熟练时,会继续分享一篇关于较进阶的文章,希望大家喜欢。