Kotlin学习笔记之 28 协程基础

285 阅读4分钟

首发于公众号: DSGtalk1989

28.协程基础

  • 准备工作

    如果你使用的是Android studio,在build.gradle文件中,添加协程依赖。

 dependencies {
   		implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
}

intelliJ IDEA中需要进入到module settingdependency添加maven依赖同上

  • 最常用的协程启动

    CoroutineScope.launch,我们可以看一下这个方法,有三个参数。

    public fun CoroutineScope.launch(
          context: CoroutineContext = EmptyCoroutineContext,
          start: CoroutineStart = CoroutineStart.DEFAULT,
          block: suspend CoroutineScope.() -> Unit
      ): Job {
          val newContext = newCoroutineContext(context)
          val coroutine = if (start.isLazy)
              LazyStandaloneCoroutine(newContext, block) else
              StandaloneCoroutine(newContext, active = true)
          coroutine.start(start, coroutine, block)
          return coroutine
      }
    

    三个参数分别代表着,协程的上下文,启动模式和挂起的函数。

    协程的上下文和启动模式都有默认值,不填写直接走默认。并且该方法最终会返回一个Job对象,我们点进去看一下。

    方法和参数不多,我们逐个的去看。

    • cancel取消一个协程

      fun main() {
             val job = GlobalScope.launch {
                 delay(1000L)
                 println("World!")
             }
             job.cancel()
             println("Hello,")
         }
      

      只有Hello,,没有World!。因为协程被取消了。

    • join等待协程执行完毕

      fun main() = runBlocking {
             val job = GlobalScope.launch {
                 delay(1000L)
                 println("World!")
                 delay(1000L)
             }
             println("Hello,")
             job.join() 
             println("Good!")
         }
      

      和java中的Thread.join比较像,都是会阻塞当前线程,所以最终打印出来的是

      Hello,
      World!
      Good!
      

      这里需要注意,join方法用suspend修饰,是个挂起函数,挂起函数必须只能在协程中进行使用,所以外面包裹了一个runBlocking,具体runBlocking的含义我们之后分析。

    • cancelAndJoin取消一个任务并且等到他彻底结束

      public suspend fun Job.cancelAndJoin() {
         cancel()
         return join()
      }
      

    OK,我们回过来继续看一下协程的概念,本身的协程是非常的轻量级的线程。一开始我们就可以把它理解成一个与当前线程区别开来的线程。

  • runBlocking

    很明显,GlobalScope的生命周期是全局的,即,完全起了一个线程,什么时候走完完全取决于整个应用程序的生命周期。我们来看这样一个效果

    fun main() {
          println("ready") 
          GlobalScope.launch {
              println("GlobalScope.launch go!")
              delay(5000L) 
              println("GlobalScope.launch end!")
          }
          println("end")
      }
    

    由于主线程很快的就结束掉,我们完全没有给到协程启动的机会,jvm迅速的完成了主线程的任务。导致我们看到的控制台输出是这样的

    ready
    end
    

    实际上这里面我们希望的是能够让main方法把协程里面的内容跑完再结束的,这个时候就轮到我们的runBlocking登场。

    Runs new coroutine and blocks current thread interruptibly until its completion.

    即起一个新的协程,并且立马阻塞当前的线程,直到协程完毕。

    这里我们需要来认识一下,协程中的sleep函数delay,该函数只能用在挂起函数中,会让协程暂停一段时间。

    那么这样一来,我们结合起来看

    fun main() {
          println("ready") // 当协程在后台等待时, 主线程继续执行
    
          runBlocking {     // 但是这个表达式阻塞了主线程
              println("runBlocking start")
              delay(2000L)  // ……我们延迟 2 秒来保证 JVM 的存活
              println("runBlocking  end")
          }
      
          println("end")
      }
    

    能够很清晰的看到,主线程被阻塞了,因为runBlocking的概念,这样一来就可以达到delay阻塞了主线程的效果。

  • 协程中叠协程

    协程中我们可以再起协程,举个例子,我们瞎看如下代码。

    fun main() {
          println("ready") // 当协程在后台等待时, 主线程继续执行
          runBlocking {
              // 但是这个表达式阻塞了主线程
              GlobalScope.launch {
                  // 在后台启动新的协程, 然后继续执行当前程序
                  println("GlobalScope.launch go!")
                  delay(5000L) // 非阻塞, 等待 1 秒 (默认的时间单位是毫秒)
                  println("GlobalScope.launch end!")
              }
              println("runBlocking start")
              delay(1000L)  // ……我们延迟 2 秒来保证 JVM 的存活
          }
          println("end")
      }
    

    经过上文的分析,我们知道runBlocking会阻塞当前的线程,直到运行完它自己的协程,结果现在我们在协程中又通过GlobalScope.launch起了一个全局的协程。考虑到前面讲到的生命周期问题,我们猜测runBlocking不会去等GlobalScope.launch,事实也确实如此。

    ready
      runBlocking start
      GlobalScope.launch go!
      end
    

    我们现在去掉GlobalScope,直接走launch。发现明显不同的生命周期,直接launch的话,这个协程的生命周期就是runBlocking,也就是说,哪怕launch里面消耗的时间再长,main函数也需要等到整个的runBlocking生命周期走完即launch里面的协程也结束才行。

  • 协程到底如何轻量

    repeat(10_000) {
          // 启动大量的协程
          runBlocking {
              launch {
                  print(".")
              }
          }
      }
    

    启动一万个协程,轻飘飘的完成了打点的任务。