Kotlin协程概览

Posted warmor

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin协程概览相关的知识,希望对你有一定的参考价值。

  协程( Coroutines)并不是 Kotlin 提出来的新概念,很多的编程语言都有实现,如:Go、Python 等。。本文所讲,专指kotlin的协程。

  在android 11中,Asynctask已经被废弃了,因为协程可以更简单,直观的实现异步任务。而且协程是谷歌推荐的异步处理机制,那么什么是协程呢?其实很简单,就是kotlint封装的一套线程api(线程框架),类似于 Java 中的 Executor 和 Android &Java中的 AsyncTask、Handler。
  协程的创建方式有三种,async,launch和runBlocking。如下:

launch getName() 

var name = async getName()  
Log.i(TAG, "name1 is " + name.await())

runBlocking getName() 

  runBlocking的方式,在开发中基本不会用到,因为它是线程阻塞的。launch是最常用的,async有时也会用到。用async创建,会有一个 Deferred 类型的返回值,需要使用 返回值.await()来获取返回值,如果不用,不等async返回,就执行下面的打印语句了。
  但是launch和async不可以直接使用,需要在协程作用域里面才能用,有下面三种方式创建协程作用域。

runBlocking 
    getName()

//通过使用 GlobalScope 单例的方式
GlobalScope.launch 
    getName()


// 参数类型为 CoroutineContext ,Dispatchers.Main 和 Job 都是CoroutineContext 类型
val scope = CoroutineScope(Dispatchers.Main + Job())
//取消协程:job.cancle() 判断状态:job.isActive job.isCancelled isCompleted
val job = scope.launch 
    getName()

  runBlocking的方式上面说过了,方法二不会阻塞线程,但是也不推荐这种用法,因为它的生命周期和 app 一样,且不能取消,推荐使用第三种方式。来看下CoroutineScope的参数。
  Dispatcher 用于告知协程,应该在哪个线程中运行。
Dispatchers.Main 就是在 Android 的主线程中运行,除了 Main 之外,我们还可以指定:
Dispatchers.IO:对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,如:读写文件,操作数据库和网络请求
Dispatchers.Default:适合 CPU 密集型的任务,比如运算
  Job是协程的唯一身份标识,可以用来控制和判断协程的状态。
  下面来具体看看协程到底有多简洁,方便。(注:launch代码块就是协程)

  val scope = CoroutineScope(Job()+Dispatchers.Main)
  val job1 = scope.launch 
  	// xxxxxxxxx location 1 运行在主线程
  	var name =  getName() // location 2 运行在IO线程
  	var pwd = getPwd() // location 3 运行在IO线程
  	textView1 = name //location 4 运行在主线程
  	textView2 = pwd
  
  private suspend fun getName() = withContext(Dispatchers.IO) 
	//        delay(4000)
    val name: String = getNameFromNetwork();//耗时操作
    name
  
  private suspend fun getPwd() = withContext(Dispatchers.IO) 
	//        delay(4000)
    val pwd: String = getPwdFromNetwork();//耗时操作
    pwd
  

  以前进行网络操作时,基本都是用的回调,有了协程,我们就可以直接获取,然后直接使用。而且不同的线程可以写在一处,这就是神奇的协程!
  有几处需要说明一下:

withContext()是kotlin内置的suspend(挂起函数),可以指定它所包含的代码块运行在哪个线程,很常用的,代码块的最后一行就是返回值。
getName()是自定义的挂起函数,需要在挂起函数或者协程中调用。

  说下这段代码的执行流程:

当执行到location 1时,运行在主线程
当执行到location 2时,就兵分两路了,一路是主线程,一路是协程(此处是IO线程)
主线程跳出协程,也就是跳出launch代码块,执行后续代码
协程(IO线程)就可以执行它的耗时的动作了
IO线程执完毕时,切回到主线程,执行location 4

  所以协程的本质就是启动一个新线程,然后执行新的线程,也就是suspend函数(不影响主线程,主线程该干嘛干嘛),等新的线程执行完毕,再切回(合并)到主线程,也就是resume。
  为什么suspend函数直接或者间接的被协程调用呢?这是因为切换回主线程的动作只有协程才能做。这么看来,suspend函数的作用只是切换到IO线程咯,没那么简单!在getName()函数中也可以写withContext(Dispatchers.Main),那就不用切线程了,那么suspend函数到底有什么用呢?其实就是一个提醒,suspend函数的创建者提醒调用者–本函数可能会有耗时操作,需要在协程里面调用。
  为什么都说suspend函数是非阻塞式挂起呢?道理很简单,因为挂起不影响主线程啊,只不过是开启了一个新的线程做耗时操作而已。需要说明的是,自定义 的suspend函数,需要调用系统自带的挂起函数,如果不调用,那么它就没有存在的意义了。
  其实上面的代码还有改进的空间,getPwd()是要等getName()执行完毕才会接着执行,因为它俩运行在同一个线程,这样显然是有些浪费时间的,所以可以进行如下改造:

  var name =  async getName()  // location 2 运行在IO线程
  var pwd = async getPwd()  // location 3 运行在IO线程
  textView1 = name.await() //location 4 运行在主线程
  textView2 = pwd.await()

  这样在协程内部又创建了两个子协程,如此一来,getPwd()和getName()就运行在两个不同的线程,而且使用了await()就是等asuync返回了再去赋值。
  协程内部再创建协程,那么新创建的协程就是原来协程的子协程,举例说明:

  scope = CoroutineScope(Job()+Dispatchers.Main)
  val job1 = scope.launch  // No.1
  	// xxxxxxx location 1
  	var name =  getName() 
  	// xxxxxxx location 2
  	var job2 = launch // No.2
		var jo4 = launch // No.4
		
	
	var job3 = launch // No.3
	
  
  private suspend fun getName() = withContext(Dispatchers.IO) 
  	if(!scope.isCancelled)  // ensureActive()
		//        delay(4000)
	    val name: String = getNameFromNetwork();//耗时操作
	    name
     else 
    	""
    
  

  上面的代码中一个创建了4个协程,No.2 No.3的父协程是No.1,它俩是兄弟协程,No.4的父协程是No.2。协程的这种特性叫做结构化并发,这个特性,使得管理起来很是方便,比如,取消所有协程,只需要调用:scope.cancel()就可以了。要取消No.2 No.4,只需要调用 job2.cancel()。取消操作是取消自己和它的子协程。
  如果协程正在执行时被取消了,拿上面的例子来说,如果执行到location 1,协程被取消,那么后面的都不执行了,如果正在执行getName()那么会执行完该函数,后面的不再执行,等getName()执行完毕,系统还会再自动检测协程是否完成(withContext的功能),如果完成了,后面的也不执行了。
  kotlin官方有这么一个对比:

repeat(100000) 
  launch 
    delay(1000)
    println(".")
  

repeat(100000)
  Thread
    Thread.sleep(1000)
      println(".")
  

  开启十万个协程与开启十万个线程对比,协程可以正常执行,而线程却出现了内存溢出,官方以此来说明协程比线程更加轻量级,其实这是不对的,因为协程是开启了一个线程池来运行线程的,所以正确对比应该是下面这样:

val executors = Executors.newSingleThreadScheduledExecutor()
var task = Runnable
  println(".")

repeat(100000) 
  executors.schedule(task, 1, TimeUnit.SECONDS)

协程 VS 线程池,性能不相上下。
  最后再来说下使用协程所需要的依赖吧!
根目录下的 build.gradle :

buildscript 
  ...
  ext.kotlin_coroutines = '1.3.1'
  ...

Module 下的 build.gradle :

dependencies 
  ...
  // 依赖协程核心库
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
  // 依赖当前平台所对应的平台库
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
  ...

  好了,到这里,kotlin的协程就讲的差不多了,来总结一下,kotlin协程就是一个线程框架,它可以启动并且切换线程,而且在线程执行完毕后,还可以再切回来,代码看起来很简洁,用阻塞式的代码实现了非阻塞的效果。

以上是关于Kotlin协程概览的主要内容,如果未能解决你的问题,请参考以下文章

kotlin协程

Kotlin 协程 Basics

Kotlin协程简介

Kotlin协程简介

Kotlin协程简介

Kotlin协程简介