【kotlin】- 携程基本使用

作者: 拔萝卜占坑 | 来源:发表于2021-09-13 11:20 被阅读0次

    简介

    随着kotlin不断普及,以其简洁的语法糖,易扩展,空安全,汲取了不同语言的优点等...越来越受到开发者的青睐。刚入kotlin,除了和Java不一样的语法让人难以习惯外,“携程”和“泛型”更是让开发者头疼。接下来由我带大家了解kotlin携程基本使用。

    其它文章

    【kotlin】- delay函数实现原理
    【kotlin】- 携程的执行流程
    【kotlin】- 携程的挂起和恢复

    创建携程

    • \color{blue}{kotlin中使用Thread}
      如果在kotlin使用Thread创建线程,还在像Java那样new一个Thread对象,似乎缺乏违和感。kotlin提供了直接创建线程的方法。

      fun main() {
          thread(start = true,isDaemon = false){
              println("${treadName()}=====创建一个线程")
          }
      }
      

      输出:

      Thread-0=====创建一个线程
      
      Process finished with exit code 0
      

      是不是比Java的方式要简便许多,isDaemon指定线程是否是守护线程,如果这里指定为true日志是打印不出来的哟,原因可以百度一下守护线程

    • \color{blue}{启动一个全局携程}

      fun main() {
          // CoroutineScope(英文翻译:携程范围,即我们的携程体)
          GlobalScope.launch (CoroutineName("指定携程名字")){
              delay(1000)
              println("${Thread.currentThread().name}======全局携程~")
          }
      }
      

      很简单的一个例子(官方例子main最后调用了sleep延迟函数),运行main,发现在控制台并没有打印协调体中的日志,输出如下:

      Process finished with exit code 0
      

      使用官方例子

      fun main() {
          GlobalScope.launch (CoroutineName("指定携程名字")){
              delay(1000)
              println("${Thread.currentThread().name}======全局携程~")
          }
          Thread.sleep(2000L)
          println("${Thread.currentThread().name}======我是最后的倔犟~")
      }
      

      运行输出如下:

      DefaultDispatcher-worker-1======全局携程~
      main======我是最后的倔犟~
      
      Process finished with exit code 0
      

      解释

      从第打印图可以看出,携程创建了新的线程DefaultDispatcher-worker-1来执行,不在主线程,所以全局携程体和全局携程体外的代码是在不同线程中异步执行的。
      全局携程创建的是守护线程,而主线程不是,所以当进程中所有非守护线程执行完,进程就会退出,守护进程也将不复存在。这就是为什么上面例子不能执行打印代码的原因。

      守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束

      kotlin携程创建的线程对象是CoroutineScheduler中Worker内部类,看一下这个内部类的初始化。默认就是守护线程。如果大家想要验证,可以使用jps打印当前执行的Java进程,在用jstack查看进程中相关线程的情况。

      internal inner class Worker private constructor() : Thread() {
         init {isDaemon = true}
      }
      
    • \color{blue}{启动子携程}

      // runBlocking协程构建器将 main 函数转换为协程
      fun main(): Unit = runBlocking {
          launch {
              delay(1000)
              println("${treadName()}======局部携程~")
          }
      }
      

      launch是CoroutineScope的扩展函数,所以必须在携程体内才可以调用。输出如下:

      main======局部携程~
      
      Process finished with exit code 0
      

      runBlocking会阻塞当前线程并且等待,在所有已启动的子协程执行完毕之前不会结束。所以launch启动就是runBlocking子携程,因为launch在runBlocking携程作用域中。在看一个例子:

      fun main(): Unit = runBlocking {
          GlobalScope.launch {
              delay(2000L)
              println("${treadName()}======全局携程")
          }
          // 如果没有下面的代码,上面代码不会执行
          launch {
              delay(1000L)
              println("${treadName()}======局部携程")
          }
      }
      

      输出如下:

      main======局部携程
      
      Process finished with exit code 0
      

      解释

      GlobalScope.launch启动是全局携程,会重新新建一个线程来执行,并不是runBlocking的子携程。所以并不会等待GlobalScope.launch携程体执行完再退出进程。

    • \color{blue}{coroutineScope声明携程作用域}

      suspend fun main() {
          // 声明携程作用域,挂起函数,会释放底层线程用于其他用途,创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束
          coroutineScope {
              // 在该携程作用域启动携程
              launch {
                  delay(3000L)
                  println("${treadName()}======才开始学习coroutines")
              }
          }
          println("${treadName()}======最后的倔犟~")
      }
      

      这种方式启动的携程作用域就在coroutineScope内。注意日志线程名字,输出如下:

      DefaultDispatcher-worker-1======才开始学习coroutines
      DefaultDispatcher-worker-1======最后的倔犟~
      
      Process finished with exit code 0
      

      从日志发现,main居然不上在主线程执行的,其实并不是这样,反编译kotlin代码,发现main主入口代码变成这样了RunSuspendKt.runSuspend(new KotlinShareKt$$$main(var0))。其实coroutineScope就是创建一个携程环境。

      在看一个复杂的点的例子

      fun main() = runBlocking { 
          launch {
              delay(2000L)
              println("${treadName()}======Task from runBlocking")
          }
          coroutineScope { // 创建一个协程作用域
              launch {
                  delay(1000L)
                  println("${treadName()}======Task from nested launch")
              }
      
              delay(100L)
              println("${treadName()}======Task from coroutine scope") // 这一行会在内嵌 launch 之前输出
          }
          println("${treadName()}======scope is over")
      }
      

      输出如下:

      main======Task from coroutine scope
      main======Task from nested launch
      main======scope is over
      main======Task from runBlocking
      
      Process finished with exit code 0
      

      解释

      launch {...}执行了挂起函数delay,而coroutineScope{...}可以看着也是一个子携程体,调用挂起函数delay。而launch {...}和coroutineScope{...}后面的代码谁先执行就要看launch中delay延迟的时间了。

    • \color{blue}{CoroutineScope构建携程}

      fun main() {
          val cs = CoroutineScope(Dispatchers.Default)
          cs.launch {  }
      }
      
    • \color{blue}{withContext在指定携程上下文启动携程}
      使用给定的协程上下文调用指定的挂起块,挂起直到它完成,并返回结果

      fun main() = runBlocking {
          val result = withContext(Dispatchers.Default) {
              delay(3000)
              println("${treadName()}======1")
              30
          }
          println("${treadName()}======$result")
      }
      

      输出如下:

      DefaultDispatcher-worker-1======1
      main======30
      
      Process finished with exit code 0
      
    • \color{blue}{Android在生命周期内启动携程}
      需要引入库

      androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02
      

      使用:

      lifecycleScope.launch {}
      
    • \color{blue}{携程超时}
      在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。

      withTimeout(1300L) {
         repeat(1000) { i ->
             println("I'm sleeping $i ...")
             delay(500L)
         }
      }
      

      扩展
      由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeoutwithTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull通过返回 null 来进行超时操作,从而替代抛出一个异常。

    • \color{blue}{组合挂起函数}

      suspend fun doSomethingUsefulOne(): Int {
          delay(1000L) // 假设我们在这里做了一些有用的事
          return 13
      }
      suspend fun doSomethingUsefulTwo(): Int {
          delay(1000L) // 假设我们在这里也做了一些有用的事
          return 29
      }
      
      • 默认顺序调用
        val time = measureTimeMillis {
            val one = doSomethingUsefulOne()
            val two = doSomethingUsefulTwo()
            println("The answer is ${one + two}")
        }
        println("Completed in $time ms")
        
      • async 并发
        val time = measureTimeMillis {
          val one = async { doSomethingUsefulOne() }
          val two = async { doSomethingUsefulTwo() }
          println("The answer is ${one.await() + two.await()}")
        }
        println("Completed in $time ms")
        
      • 惰性启动的 async
        可选的,async可以通过将 start 参数设置为 CoroutineStart.LAZY而变为惰性的。 在这个模式下,只有结果通过 await获取的时候协程才会启动,或者在 Job 的 start`函数调用的时候。
        val time = measureTimeMillis {
           val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
           val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
           // 执行一些计算
           one.start() // 启动第一个
           two.start() // 启动第二个
           println("The answer is ${one.await() + two.await()}")
        }
        println("Completed in $time ms")
        
    • \color{blue}{携程join}
      依然例子先行:

      fun main() = runBlocking{
          val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
              delay(1000L)
              println("World!")
          }
          println("Hello,")
          job.join() // 等待直到协程执行结束
      }
      

      输出如下:

      Hello,
      World!
      
      Process finished with exit code 0
      

      按照之前的讲解,GlobalScope.launch启动的是全局携程,并不属于runBlocking的子携程,所以runBlocking不会等待该携程执行完毕再退出进程,那为什么这里会等待呢,那这就是join函数的功劳,join作用是挂起协程直到携程执行完成。

    • \color{blue}{携程取消}
      协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的。

      val startTime = System.currentTimeMillis()
      val job = launch(Dispatchers.Default) {
          var nextPrintTime = startTime
          var i = 0
          while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
              // 每秒打印消息两次
              if (System.currentTimeMillis() >= nextPrintTime) {
                  println("job: I'm sleeping ${i++} ...")
                  nextPrintTime += 500L
              }
          }
      }
      delay(1300L) // 等待一段时间
      println("main: I'm tired of waiting!")
      job.cancelAndJoin() // 取消一个作业并且等待它结束
      println("main: Now I can quit.")
      

      打印输出并没在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因

    • 在 finally 中释放资源

      fun main() = runBlocking {
          val job = launch {
              try {
                  repeat(1000) { i ->
                      println("job: I'm sleeping $i ...")
                      delay(500L)
                  }
              } finally {
                  println("job: I'm running finally")
              }
          }
          delay(1300L) // 延迟一段时间
          println("main: I'm tired of waiting!")
          job.cancelAndJoin() // 取消该作业并且等待它结束
          println("main: Now I can quit.")
      }
      
    • 运行不能取消的代码块
      在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用 'withContext'函数以及 NonCancellable上下文。

      val job = launch {
          try {
              repeat(1000) { i ->
                  println("job: I'm sleeping $i ...")
                  delay(500L)
              }
          } finally {
              withContext(NonCancellable) {
                  println("job: I'm running finally")
                  delay(1000L)
                  println("job: And I've just delayed for 1 sec because I'm non-cancellable")
              }
          }
      }
      delay(1300L) // 延迟一段时间
      println("main: I'm tired of waiting!")
      job.cancelAndJoin() // 取消该作业并等待它结束
      println("main: Now I can quit.")
      

    结束语

    很多例子都是官网的,只是加上一些自己的理解,这篇文章只是带大家快速入门kotlin携程使用,后面会逐步深入,讲解携程的实现原理。

    相关文章

      网友评论

        本文标题:【kotlin】- 携程基本使用

        本文链接:https://www.haomeiwen.com/subject/wxiiwltx.html