Android的协程学习笔记之一

542 阅读4分钟

Kotlin的协程是什么?

  • Kotlin的协程在 JVM上是线程框架,但它的概念层不仅仅是线程框架

  • 协程是一种在程序中的处理并发任务的方案;也是这种方案的一个组件; 可以理解为一个Adapter。

  • 协程和线程属于一个层级的概念,是一种不同于线程的概念,用于解决并发

    • 协程中不存在线程,也不存在并行(注意:并行和并发不是同一个概念) 并行:Parallelism:导致线程安全问题,如开两个线程给两个数组进行筛选处理 并发:Concurrency:如后台发请求,前端显示请求信息

    • Kotlin的协程不需要处理以上的问题,但依然存在java的线程安全问题,所有基于JVM的语言,Java,Scala,Groovy等都无法避免此问题

  • Kotlin For Java 的协程不同于广义的协程

协程的写法

  • 开启协程和线程写法比较 例子:

              //开启一个协程(非常规写法)
              GlobalScope.launch {
                  print("线程的名称1:${Thread.currentThread().name}")
              }
              //开启并运行一个线程
              Thread{
                  print("线程的名称2:${Thread.currentThread().name}")
              }.start()
              //KTX 的线程写法:
              thread {
                  print("线程的名称3:${Thread.currentThread().name}")
              }
    

    可见的是,如果仅仅是开启一个后台任务,协程并没有语法上的优势,甚至于增加了拓展函数包之后,开启线程来运行一个后台任务更加的方便,但协程的优势并不是在此,而是在其的可挂起上和线程切换上。 下面的示例IoCode()代表一个后台任务, uiCode()代表一个前台任务,于是在线程上我们就要这样子做线程切换:

thread { 
                ioCode1()
                runOnUIThread{
                    uiCode1()
                    thread { 
                        ioCode2()
                        runOnUiThread{
                            uiCode2()
                        }
                    }
                }
            }

可以看见的是缺点很明显,代码可读性低,写起来烧脑,且难以维护,套娃,代码难看。业务逻辑复杂后引起回调地狱。

以下用协程来实现:

  1. 首先指定运行的方法为挂起函数(加上关键字suspend),并且给他们指定运行的线程(Dispatchers.IO,Dispatchers.Main等)
  2. 将挂起函数在协程中运行
  //指定该函数在IO线程执行
    private suspend fun ioCode()= withContext(Dispatchers.IO){
        //业务逻辑
    }
    
    //指定该函数在ui线程执行
    private suspend fun uiCode()= withContext(Dispatchers.Main){
        //前端业务逻辑
    }
   ....
GlobalScope.launch { 
                ioCode()
                uiCode()
            }
            otherCode()
            //其他函数
            

执行的顺序,ioCode执行之后,uiCode才会执行,协程即GlobalScope.launch{}内的逻辑不会阻塞后面的代码(otherCode())的执行,协程会独立给予他们一个线程环境去运行,并且可以指定这个运行环境如下:

   GlobalScope.launch(Dispatchers.Main) { 
          //该代码块内的环境为主线程
            }
    GlobalScope.launch(Dispatchers.IO) { 
          //该代码块内的环境为IO线程
     }

那么问题来了,如果一个方法内需要执行的逻辑需要多次切换呢?这其实也很简单

   suspend fun  testFunc(){
        withContext(Dispatchers.IO){
            //IO线程内的操作
            func1()
        }
       withContext(Dispatchers.Main){
           //UI线程中的操作
           func2()
       }
       withContext(Dispatchers.IO){
           //IO线程内的操作
           func3()
       }
    }
     GlobalScope.launch(Dispatchers.Main) { 
          //该代码块内的环境为主线程
          testFunc()
            }

协程会保持的代码的时序性质,即示例代码中的func1func2fun3会依次执行,且会在我们设定的线程中运行。 总结一下: 1. 用lauch()开启一段协程,并指定运行环境,通常为Dispatchers.Main) 2. 把需要后台工作的函数写成suspend挂起函数,可以在内部使用withContext或者其他挂起函数来切换线程 3. 将挂起函数写在协程之中,一条线,优雅方便且读性强

协程的天然优势:性能

  • 程序什么时候需要切换线程
    • 工作比较耗时:放在后台

      • 问题节点:如何判断该方法耗时还是不耗时? 很多我们不熟悉或者同学写的看不懂(垃圾)的代码无法判断其耗时或者不耗时,但我们没有条件去逐个测试其耗时性,或者有些方法耗时不稳定,难以测试。但有了协程,就避免了这个问题,所有有耗时的函数可以全写在协程之中,就避免了所有的非UI渲染的耗时操作,且不影响可读性。
      • 这使用Java基本无法实现。
    • 工作比较特殊:放在指定线程 —— 一般我们放在了主线程,如UI操作

协程 For JVM的概念

  • 线程框架
  • 可以用看上去同步的代码写出异步的操作
    • 优势1:耗时函数自动后台,从而提高性能
    • 优势2:线程的“自动切回”

协程和线程,线程和协程安全的关系?

suspend

  • 并不是用来切线程的
  • 仅仅是一个标记和提醒,这是一个挂起函数(语法层)