kotlin之协程(coroutines)学习

Posted microhex

tags:

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

文章目录


两个月没写博客了,除了辞职找工作,就是熟悉新的环境,新的项目一直加班,累并快乐着,还是要好好加油啊。

本文翻译来自kotlin官方文档
https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

kotlin,最为一门编程语言,在它的标准类库中只提供了最低级别AP来支持其他类库使用协程。
不像其它有类似协程能力编程语言那样,async(异步)和await(等待)在kotlin并不是关键字,它们甚至不是kotlin标准类库的一部门。

此外,在kotlin的概念中,终端函数为异步操作 提供了一种更安全,更少错误的抽象。

JetBrains为kotlin的协程开发了一个丰富的类库kotlinx.coroutines,它包含了一系列的高级的协程机制,其中就包括了launch,async和其它机制。

本文提供了一系列的例子还介绍协程(kotlinx.coroutines),它分成了不同的主题。

为了使用好协程,同时也为了跟上这些例子,首先你需要添加协程core类的依赖。具体地址为:

https://github.com/kotlin/kotlinx.coroutines/blob/master/README.md#using-in-your-projects

如果你只想先了解或者熟悉一下coroutines,使用如下配置即可:

  1. android project root的build.gradle配置koltlin如下:
buildscript 
    ext.kotlin_version = '1.3.41'
    
    repositories 
        google()
        jcenter()    
    
    
    dependencies 
        classpath 'com.android.tools.build:gradle:3.4.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    

  1. app build.gradle下配置coroutines-core,当前最新版本为:1.3.0-RC
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

dependencies 
	//...other implementation....

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    // coroutines-core
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC'
    // coroutines-android
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-RC'



协程基础

你的第一个协程代码

下面就是你的第一行协程代码:

import kotlinx.coroutines.*

fun main() 

	//后台启动一个新的协程
    GlobalScope.launch 
		// 协程非阻塞延迟1s
        delay(1000L)

        println("hello world")
    

    println("hi")

	//主线程等待3s,同时协程在等待2s
    Thread.sleep(3000L)

结果为:
从本质上来讲,协程(coroutines)只是轻量级的线程。它们在CoroutineScope上下文中通过launch协程builder启动。这里通过GlobalScope方式可以启动一个新的协程(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) ,那么意味着新启动协程的生命周期只会受限于整个应用的生命周期(换句话说使用GlobalScope启动的协程就像Application的生命周期一样,伴随的应用而开始,随着应用的死亡而消失)。

实现以上代码类似的结果,你可以使用GlobalScope.launch … 和 thread … 或者使用 delay … 和 Thread.sleep … 替代,你可以尝试一下。

但是如果你如果使用thread来替代 GlobalScope.launch,编译器就会报如下错误:

Suspend functions are only allowed to be called from a coroutine or another suspend function
翻译过来就是:
使用Suspend 修饰的中断函数 只能在协程中 或者其他使用Supspend中断函数中 使用,在thread中不能使用。

这是因为deley是一种特殊的终端函数,它并不阻塞线程,但是会中断阻塞协程。所以它只能用在协程中。

阻塞&非阻塞之间的桥梁

在上面的例子中,我们展示了使用混合的非阻塞方式 delay(…) 和 阻塞方式 Thread.sleep (…) 的代码。但是这样的代码很容易失去控制,特别是在阻塞和非阻塞混合代码中。我们通过runBlocking协程构造器显式使用阻塞:

fun main() 

    GlobalScope.launch 

        delay(1000L)

        println("hello world")
    

    println("hi")

	// 这里会阻塞主线程
    runBlocking 
        delay(3000L)
    

运行结果和上面是一致的,但是这里的代码只使用了非阻塞式delay。主线程通过调用 runBlocking来等待协程内部逻辑执行完成。

这个例子还可以写成更符合习惯的代码,使用runBloking包装执行main函数:

fun main() = runBlocking<Unit> 

    GlobalScope.launch 
        delay(1000L)
        println("hello world")
    

    println("hi")
    delay(3000L)


这里的runBlocking … 作为一个适配器来启动一个顶级的主协程。我们明确的指定Unit作为返回值类型,因为在kotlin一个好的main函数需要一个返回值。

当然了,你也可以使用单元测试Junit来测试中断函数了。

class MyTest 
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> 
        // here we can use suspending functions using any assertion style that we like
    

等待一个任务

使用delay多长时间来等待协程完成并不是一个好的方法(这个是显然的,上面的例子中我们有很多时间是浪费的),那我们就明确等待,等待的时长就是后台协程完成的时间。

fun main() = runBlocking 

    val job = GlobalScope.launch 
    
        println("job start....")
        delay(2000)
        println("job end....")
        
    

    println("main thread...")
    
    //等待协程完成,主线程即中断
    job.join()

结果为:

主协程等到子协程完成之后,就退出程序,这个代码比上面的代码要好。()

结构化并发

但是实际过程使用协程还存在一些不足之处。当我们使用GlobalScope.launch时,我们创建了一个顶级的协程。
即使它是轻量级的,但它在运行时还是要消耗一些内存资源。如果我们忘记保留对新启动的协程持有引用,那么它将会持续运用。

如果代码在协程中挂起(例如,我们错误的延迟太长时间)我们该怎么办?如果我们启动了太多的协程,消耗完了内存我们该怎么办?手动添加对所有启动协程的引用,然后join它们(等待它们结束)是非常容易出错的。

这里有一个很好的解决办法。我们可以在代码中使用结构化并发。这次并不是使用GlobalScope启动协程,
就像我们平时使用线程那样(线程总是全局概念的),我们在指定的scope内启动协程。

在我们的例子中,我们使用runBlocking协程构建器将main函数转变成了一个协程函数。每一个协程构建器,包括runBlocking,都会添加一个CoroutineScope实例到这个Scope中。我们可以在这个Scope中启动协程,并不需要明确的使用join协程(等待协程结束),因为一个外部的协程(我们的例子是runBlocking)会等到内部的所有的协程完成之后才会完成。因为我们可以使我们的例子更为简单:

fun main() = runBlocking 
    launch 
        // 在runBlocking启动一个新的协程
        delay(2000)
        println("world")
    
    
    println("hello")

Scope 构造器

(Scope我们可以理解为范围,类似于Java代码中全局属性/局部属性的含义)

除了了通过不同的协程构建器构建协程scope,你还可以通过coroutineScope来定义自己的scope。它能创建一个协程scope,会等到所有的子协程完成之后
自身才会完成。runBlocking和coroutineScope两种Scope最大的不同是,coroutineScope在等待子协程完成时,并不是阻塞当前线程。举个例子:

fun main() = runBlocking 

    launch 
        delay(2000L)
        println("task from runBlocking")
    

    coroutineScope
        launch 
            delay(400L)
            println("task from nested launch")
        

        delay(100L)
		
		//由于coroutineScope并不会阻塞当前线程,所以这个是最先打印的。
        println("task from coroutine scope")
    
    
    // 这里会等到 nested launch 完成之后 才会打印
    // 所以它是阻塞式的,会等到所有的子协程完成之后 自身才会完成
    println("Coroutine scope is over")


运行结果如下:

提取函数重构

我们可以将launch中的代码块抽取为一个单独的函数。当你执行抽取函数时,新的函数会有一个suspend修饰符。这将会是你的第一个延迟函数。延迟函数可以用在协程中,就像普通函数那样使用。但是它额外的功能就是它们能反过来调用其他的延迟函数,就像下面例子中的delay一样,在协程中延迟执行功能。

fun main() = runBlocking 
    launch 
        doWorld()
    

    print("main world.")


suspend fun doWorld() 
    delay(1000)
    print("hello world")

运行结果为:

但是,如果提取的函数包含在当前作用域上调用的协程构造器,那我们该怎么办呢?这种情况下只在抽取的函数前面加一个suspend修饰符是不够的。其中的一个解决方式就是将doWorld作为CoroutineScope的一个扩展方法,但是它不是总是有效的,因为它会使API不清晰可见。而我们惯用的做法就是将显式的CoroutineScope作为包含目标函数的类中的字段,或者在外部类中实现CoroutineScope时使用隐式字符。最终的解决方案,可以使用CoroutineScope(coroutineContext) ,但是这种实现在结构上是不安全的,因为你将不会在scope中控制方法的执行,只有在私有的API中可以使用这种模式。

协程是轻量级的

执行下面的代码:

fun main() = runBlocking 
    repeat(100_000_0)
        launch 
            delay(1000L)
            print(".")
        
    


这里将会打印一百万个点,但是如果你用线程来实现上面的需求时,大概会抛出OOM了,(所以协程的高性能就显而易见了。)

全局的协程就像守护线程

下面的代码中在GlobalScope启动了一个长时间运行的协程,一秒中将会打印"I’m sleeping",主函数中延迟一段时间之后,全局函数将会退出。

fun main() = runBlocking 
    GlobalScope.launch 
        repeat(1000)
                 println("I'm sleeping $it ...")
            delay(100)
        
    

    delay(300)
    println("hello world...")


打印结果为:

我们可以看到,只打印了三行。在GlobalScope中激活的协程并不保证进程的活动状态,它们只像守护线程一样,等到主线程结束之后,就会主动结束。

以上是关于kotlin之协程(coroutines)学习的主要内容,如果未能解决你的问题,请参考以下文章

Amphp之协程助手

Amphp之协程助手

快速上手 Kotlin 开发系列之协程的挂起

快速上手 Kotlin 开发系列之协程的挂起

Python asyncio之协程学习总结

kotlin - Coroutine 协程