kotlin

Kotlin协程(Coroutine)

2021-06-07  本文已影响0人  五号先生丶

2018年年中开始接触Kotlin,从Java转到kotlin讲实话没有花费太多的时间和精力,从接触之处就被它简洁的语言风格、清晰的逻辑和强大的自带类库所吸引。到如今偶尔会去修改第三方类库都会感觉到深深的不习惯- -
从入门到今天也有两年的时间了,一直以来也没有对其做过系统的记录(主要还是懒,懒癌)。导致之前很多用过并学习研究过的东西因为时间渐渐忘记。期望以后的学习过程中能多多记录。如果描述或者理解错误的地方欢迎多多指教!

敬上Kotlin的官网地址:Kotlin中文官网

一、简述协程

本篇文章主要讲述协程的运行机制。
关于协程之所以出现已经它的出现所解决的问题,自己上官网查看,写的很详细。
官方对于协程的说明:协程

二、Andrid中引入协程的方式

在dependencies下添加依赖即可:

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

三、Kotlin的启动方式

Kotlin的启动方式有:launch(CoroutineScope.launch和GlobalScope.launch)、runBlocking、withContext、async、doAsync等。

1、launch(CoroutineScope.launch和GlobalScope.launch)创建协程

    GlobalScope.launch() {
            //TODO run something
    }

launch方法可传入三个参数context、start和block。
源码

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
}

context参数用来指定本协程运的上下文环境,即运行在哪个线程里边儿。可用参数有:

    Dispatchers.Main //主线程
    Dispatchers.Default //默认线程,那条线程里创建的那条线程执行
    Dispatchers.IO //IO线程
    Dispatchers.Unconfined //不指定线程,该协程可以由于任何线程创建、执行、挂起、结束。

start参数用来指定协程的启动模式。可用参数有:

    CoroutineStart.DEFAULT //默认启动模式。直接执行
    CoroutineStart.LAZY //调用start()方法的时候才会启动,协程在被创建时,LAZY属性会被当做延迟加载的委托。
    CoroutineStart.ATOMIC //在此启动模式下,协程具有原子性,协程在开始执行之前无法取消,在挂起阶段可取消。
    CoroutineStart.UNDISPATCHED //实验性API,没用过。。。

block参数为协程的执行体,没啥好说的。被定标记为suspend耗时操作。

通过context参数和start参数可以组合成不同的任务执行流,根据不同的业务和代码逻辑需求选择不同的组合可以在较少的内存开销下方便的进行状态切换。个人觉得和RxJava类似,但是开销相较RxJava来说跟小,代码也更为精简清晰。

我们来看一个示例:

    @JvmStatic
    fun main(args: Array<String>) {
        GlobalScope.launch {
            delay(1000L)
            println("1")
        }.start()
        println("2")
    }

执行结果:

2

Process finished with exit code 0

神奇的事情发生了!
在我们所写的代码中,新建了一个协程,在协程体内演示1000ms,然后控制台输出“1”,在主线程中控制台输出“2”。可是事实上只输出了“2”,这是为何?
这里需要牵扯到一个问题,协程并非线程,launch所创建协程由线程来管理,但并非依附于线程,且并不会影响线程生命周期,所以当线程执行结束的时候,即使协程并没有执行完成也会被结束掉。接下来我们做一个验证:

    @JvmStatic
    fun main(args: Array<String>) {
        GlobalScope.launch {
            delay(1000L)
            println("1")
        }.start()

        println("2")

        Thread(Runnable {
            Thread.sleep(2000L)
            println("3")
        }).start()
    }

在上述代码中,我们在原来的代码基础上加入了线程,在线程的代码体内sleep 2000ms,然后输出“3”。看一下执行结果:

2
1
3

Process finished with exit code 0

此时先输出“2”,然后协程1000ms后正确输出了“1”,线程sleep 2000ms后正确输出了“3”。此时所有期望都被正确的输出。
解释这个原因需要提到Java中User Thread(非守护线程)和Deamon Thread(守护线程),主线程和我们刚才创建的线程都属于非守护线程,守护线程用来给非守护线程提供运行的便利,例如GC。如果JVM中所有的非守护线程都执行完毕,即JVM中已经不存在任何的非守护线程,那么JVM会杀死所有的守护线程并结束。
所以当我们执行launch时,也启动一个比它执行时间更长的线程,协程运行在哪个线程由协程的调度器决定,此时子线程并没有结束,所以协程在主线程运行结束后并没有一起结束掉,所以1秒后正确输出了结果,两秒后线程输出结果并结束。

2、runBlocking创建协程

方式为阻塞式的。其只会阻塞当前线程。比如在一个子线程中执行runBlocking,只有这个子线程会被阻塞,但是主线程不会。
runBlocking的执行会阻塞当前线程,但是runBlocking中运行的代码快会并发执行。意思就是说,当使用launch时,launch并不会已阻塞的方式来执行,而async的执行是以阻塞的方式。
代码:

runBlocking { // start main coroutine
            launch { // launch new coroutine in background and continue
                delay(1000L)
                println("World!")
            }
            println("Hello,") // main coroutine continues here immediately
            delay(2000L)      // delaying for 2 seconds to keep JVM alive
        }
        println("over")

输出:

Hello,
World!
over

Process finished with exit code 0

在runBlocking运行时阻塞主线程,所以runBlocking中的代码块先执行。在其代码块中我们执行launch新起了一个协程,由于runBlocking内部代码块并发的特性,使得先输出“Hello”,1秒后再输出“World!”,runBlocking执行完毕后,执行输出“over”。

3、withContext创建协程

只能执行在耗时任务体内,比如launch,runBlocking或者被suspend修饰的方法。通常情况下withContext的执行方式为串行,比如有1、2、3三个withContext,此时的执行顺序为1→2→3。
此外withContext可以返回耗时任务的执行结果,并且为直接返回。

    GlobalScope.launch {
            val wc1 = withContext(Dispatchers.Default) {
                println("withContext1")
                return@withContext "withContext1"
            }

            val wc2 = withContext(Dispatchers.Default) {
                println("withContext2")
                return@withContext "withContext2"
            }
        }
    Thread {
            Thread.sleep(1000L)
        }.start()

为了防止所有非守护线程结束,写了一个Thread。
执行结果:

withContext1
withContext2

Process finished with exit code 0

4、async创建协程

只能执行在协程体内,比如launch,runBlocking。通常情况下为并发执行。
此外async也可以返回耗时任务的结果,但async返回的结果为Deferred<T>,需要靠await()方法来获取返回结果。
值得一提的是,如果此时有async1,async2,在执行代码块中直接输出当前执行的async得到的结果是"并行的",但是如果在async后直接.await()得到的结果会是"串行的"。原因是在源码中await()方法被标记为了suspend,导致async会先执行被标记为suspend的耗时任务,等执行完成之后再执行下一行代码。所以会产生串行结果。
示例1并行:

      runBlocking {
            val one = async {
                var i = 0;
                while (i<10){
                    println("one:$i")
                    i++
                    delay(500L)
                }
            }

            val two = async {
                var j = 0;
                while (j<10){
                    println("two:$j")
                    j++
                    delay(500L)
                }
            }
            println("The answer is one:${one.await()} + two:${two.await()}")
        }
        println("看下线程有没有阻塞。")

示例2串行:

        runBlocking {
            val one = async {
                var i = 0;
                while (i<10){
                    println("one:$i")
                    i++
                    delay(500L)
                }
            }.await()

            val two = async {
                var j = 0;
                while (j<10){
                    println("two:$j")
                    j++
                    delay(500L)
                }
            }.await()
        }
        println("看下线程有没有阻塞。")

理解为主,没啥好说的。

结束,如发现不太对劲或者模糊的地方,欢迎随时提问指正!

上一篇下一篇

猜你喜欢

热点阅读