注册

一起掌握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)很不方便,一个两个还好管理,但是多了却不方便管理。于是我们可以使用结构化并发,这样我们可以在指定的作用域中启动协程。这点跟线程的区别在于线程总是全局的。大致如图(图片):


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协程的一个入门。当对它的使用达到熟练时,会继续分享一篇关于较进阶的文章,希望大家喜欢。


作者:敖森迪
链接:https://juejin.cn/post/7127086841685098503
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册