kotlin协程的基础笔记
  FyeYl0ESQHUh 2023年11月02日 48 0


导包

在Android 项目中需要导入:

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"

通过maven树可以分析:

|    |    |    \--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4 -> 1.4.3
|    |    |         +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3
|    |    |         |    \--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3
|    |    |         |         +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.6.10 (*)
|    |    |         |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.30 -> 1.6.10
|    |    |         \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.30 -> 1.6.10 (*)

通过上面maven 树结构分析,org.jetbrains.kotlinx:kotlinx-coroutines-android中就包含了org.jetbrains.kotlin:kotlin-stdlib。所以我们只需要导入:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"

不需要导入: implementation “org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version” 协程代码在:

org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3

创建第一个协程

fun main() {
    GlobalScope.launch {
        delay(1000)
        println("world")
        println(Thread.currentThread().name)
    }
    println("hello ")
    println("主线程:" +Thread.currentThread().name)
    Thread.sleep(2000)
}

可以看到,上面代码直接运行在main 函数中,通过delay(1000)延时一秒。通过 Thread.sleep(2000)在主线程中延时2秒。

hello 
主线程:main
world
DefaultDispatcher-worker-1

这意味着GlobalScope.launch新协程的⽣命周期只受整个应⽤程序的⽣命周期限制。先留坑

可以尝试将sleep 时间设置比delay() 设置时间小,就会发现_GlobalScope.launch{}_ 中的 println 是没有执行的。

  • delay 等待,挂起。非阻塞线程。

那么如何使用线程呢?

thread {
        Thread.sleep(500)
        println(Thread.currentThread().name)
    }

那么挂起和阻塞有什么区别呢?

  • 挂起一般是主动行为,由系统或程序发出,甚至于辅存中去,不释放CPU,但是可能释放内存。
  • 阻塞一般是被动行为,在抢占不到资源的情况下,被动挂起在内存,得到某种信号将其唤醒。(释放CPU(它的CPU被抢了,就被释放了)但是不释放内存)

我们先来看GlobalScope.launch {} 是如何创建出来的:

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
}

这个CoroutineScope.launch一共有3个入参:

  • context: CoroutineContext = EmptyCoroutineContext
  • start: CoroutineStart = CoroutineStart.DEFAULT
  • block: suspend CoroutineScope.() -> Unit

熟悉高阶函数的源码的同学都知道。CoroutineScope.() 这种写法和appy() 类似,他可以直接调用CoroutineScope中的函数,而不用写this .

然后就是EmptyCoroutineContext:

EmptyCoroutineContext 是一个特殊的 CoroutineContext,它没有任何额外的元素。CoroutineContext 是一种用于协程(Coroutines)的上下文,它可以包含一些额外的信息,例如协程的调度器(Dispatcher)、协程的名称等。

EmptyCoroutineContext 通常用于创建协程时,当你不需要指定任何额外的上下文信息时。它是一个默认的 CoroutineContext,只包含了最基本的协程元素。

使用 EmptyCoroutineContext 作为 CoroutineContext 可以确保你的协程在没有额外上下文信息的情况下正常运行。这句话很重要,没有上下文,所以可以干一个死循环在需要的时候结束,无法通过父协程直接取消子协程,那么如何取消他呢?可以看到他返回值是job,所以可以用过job 关闭,在Android 中如果不关闭,可能导致内存泄露。同时也说明了一个问题,这个参数可能和运行线程有关

然后是CoroutineStart:

  1. DEFAULT:这是协程的默认启动模式。它表示协程将立即启动,并在执行完毕后返回结果。
  2. ATOMIC:这个启动模式表示协程将作为一个原子操作执行。它将在当前线程中立即启动,并且不会切换到其他线程。
  3. LAZY:这个启动模式表示协程将延迟启动,直到明确调用协程的resume()方法。在调用resume()方法之前,协程不会执行任何操作。
  4. UNDISPATCHED:这个启动模式表示协程将在当前线程中立即启动,并且不会切换到其他线程。它与ATOMIC模式类似,但允许协程在执行过程中切换线程。

可以看到,这个用于约束协程的执行时机。所以这么一套下来,这个CoroutineScope.launch{} 会立马执行。

所以说。我们可以通过设置不同的入参控制这个job的运行时机和上下文,以达到不同的效果。

通过log 可以看到,还切换了一个子线程 DefaultDispatcher-worker-1 ,但是这里没有描述他如何指定线程的,留一坑,后期填。

那么什么是协程?

协程实际上是⼀个轻量级的线程,可以挂起并稍后恢复。协程通过挂起函数⽀持:对这样的函数的调⽤可能会挂 起协程,并启动⼀个新的协程,我们通常使⽤匿名挂起函数(即挂起 lambda 表达式)

在JVM中,那么它就是一个基于线程封装的API。所以开启1万个协程不代表开启了一万个线程。 同时GlobalScope启动的协程的生命周期基于进程的生命周期的。不存在进程死了,协程还活着的情况。

通过下列的代码,查看当前所有线程。

fun main() {
    GlobalScope.launch {
        delay(1000)
        println("world")
        println(Thread.currentThread().name)
    }
    thread {
        Thread.sleep(500)
        println(Thread.currentThread().name)
    }
    Thread.sleep(1500)
    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()

    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
    Thread.sleep(2000)
}

运行结果:

主线程:main线程总量:5
遍历的线程:main
遍历的线程:Monitor Ctrl-Break
遍历的线程:DefaultDispatcher-worker-1
遍历的线程:DefaultDispatcher-worker-2
遍历的线程:kotlinx.coroutines.DefaultExecutor

当没有协程和线程切换的代码都时候:

主线程:main线程总量:2
遍历的线程:main
遍历的线程:Monitor Ctrl-Break

桥接阻塞和非阻塞

通过上面的代码,我们看到,为了保证协程中的代码被执行,我们使用了

Thread.sleep(2000)

让主线程暂停2秒,那么是否可以通过delay 去挂起主线程呢?

fun main() {
    GlobalScope.launch {
        delay(1000)
        println("world")
        println(Thread.currentThread().name)
    }
    runBlocking {
        println("阻塞开始:"+Thread.currentThread().name)
        delay(2000)
        println("挂起结束")
    }
    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()

    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
}

上面代码打印:

阻塞开始:main
world
DefaultDispatcher-worker-1
挂起结束
主线程:main线程总量:5
遍历的线程:main
遍历的线程:Monitor Ctrl-Break
遍历的线程:DefaultDispatcher-worker-1
遍历的线程:DefaultDispatcher-worker-2
遍历的线程:kotlinx.coroutines.DefaultExecutor

通过上面的代码可以发现runBlocking{} 是阻塞了当前main 。我们直接看 runBlocking{} 的入参:

public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {}

这个入参还是有一个CoroutineContext,而且默认是EmptyCoroutineContext,那么我是不是可以换成自己协程的CoroutineContext,然后阻塞掉自己的协程,这里依旧留坑,没描述为什么可以获取到main 线程并且可以阻塞掉线程。

那么上面的代码是否有优化空间?

基于kotlin 的特性,我们将runBlocking{} 直接作为main 函数的值。

fun main()= runBlocking {
    GlobalScope.launch {
        delay(1000)
        println("world")
        println(Thread.currentThread().name)
    }
    delay(2000)
    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()

    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
}

这种写法和上面的写法的输出结果是一致的。等于说,runBlocking其实是一个切换到主线程的函数,里面调用的delay也是阻塞了主线程。同样都是5个线程,似乎没有变少。

使用job.join()

上面的代码逻辑都是类似的,都是通过延时主线程一段时间去等等协程执行完成,那么是否包含进一步的优化空间呢?

fun main()= runBlocking {
    val job= GlobalScope.launch {
        delay(1000)
        println("world")
        println(Thread.currentThread().name)
    }
    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()

    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
    job.join()// join 等待,直到子协程执行结束。
    println("job 执行结束")
}

执行结果:

主线程:main线程总量:4
遍历的线程:main
遍历的线程:Monitor Ctrl-Break
遍历的线程:DefaultDispatcher-worker-1
遍历的线程:DefaultDispatcher-worker-2
world
DefaultDispatcher-worker-1
job 执行结束

可以明显的看到,协程中都代码都执行了,但是执行的时机是调用job.join() 之后。结合上面的代码,我们可以知道主线程挂起或者阻塞后GlobalScope.launch{} 才会执行(这里说的有问题,因为这个main 函数执行完了进程就死了,所以这里需要一个挂起或阻塞,如果他没有死,那么他会执行job 中的代码,这个在协程或者自己new 一个子线程可以尝试),那么我们执行调用job.join(),是否可以佐证GlobalScope.launch{} 其实是一个挂起或者阻塞函数。

结构化并发

那么还有没有优化空间呢?协程的实际使用还有一些需要改进的地方,当我们使用globalscope.launch 时,我们会创建一个顶层协程,虽然她很轻量,但是他运行时仍然会消耗一些内存资源。如果我们忘记了保持对新启动的协程的引用。她还会继续运行。如果协程的中的代码挂机后会怎样?如果我们启动了太多的协程,并导致内存不足会怎么样?必须手动保持 对所有已启动协程的引用并join很容易出错。

有一个更好的解决办法,我们可以在代码中使用结构化并发,我们可以在执行操作所在的指定作用域内启动协程,而不是像 使用线程那样在globalscope 中启动。

在我们示例中,我们使用runBlocking 协程构建器将main 函数转为协程,包括runBlocking在内的每个协程构建器都将 CoroutineScope的实例添加到其代码块的作用域中,我们可以在这个作用域中启动协程而无需显示第join。 因为外部协程直到在作用域中启动所有协程都执行完毕后才结束。

fun main()= runBlocking {
    launch {
        delay(1000)
        println("world")
        println(Thread.currentThread().name)
    }
    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()

    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
}

执行结果:

主线程:main线程总量:2
遍历的线程:main
遍历的线程:Monitor Ctrl-Break
world
main

可以看到线程数量减少了,同时延时的操作指向的线程是主线程了。所以说,runBlocking 是一个协程。 launch 则是在协程中创建协程,所以不存在线程切换与调度。我们这里又出现了一个新的东西launch {},结合上面的CoroutineScope.() 可以知道,这个launch 其实是CoroutineScope 中的一个函数。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
){}

是吧,又有context和 start,而且他是一个job,结合上面的经验我们知道 job 并不是马上执行的。当然了我们这里主要是减少线程的Demo。

作用域构建器

通过上面的结构化并发代码,我们可以看到launch{}中的代码块是执行了的。如果说,我们需要类似于 job.join()去阻塞当前协程呢? 答案就是coroutineScope{}。他会创建一个协程作用域并却在所以已启动的协程执行完毕前都不会结束。这种和runBlocking 与coroutineScope 看起来是类似的。 因为他们都会等待其协程体以及所有子协程结束,主要的区别在于runBlocking 方法会阻塞当前线程来等待。而 coroutineScope 只是挂起。会释放底层线程用于其他用途。 基于这种差异,runBlocking是常规函数,而coroutineScope 则是挂起函数。我们结合demo 去理解。

fun main()= runBlocking {
    launch {
        //delay(1000)
        println("world")
        println("launch:"+Thread.currentThread().name)
    }
    coroutineScope {
        launch {
          //delay(500)
            println("coroutineScope launch ")
        }
        //delay(2000)
        println("coroutineScope")
    }
    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()
    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
}

执行结果:

coroutineScope
world
launch:main
coroutineScope launch 
主线程:main线程总量:2
遍历的线程:main
遍历的线程:Monitor Ctrl-Break

通过上面的日志可以看到先执行的是:coroutineScope,然后是 launch,然后是coroutineScope launch ,最后是函数 runBlocking 的后续代码。 所以说,coroutineScope挂起了runBlocking所在的协程。那么launch和coroutineScope.launch 到底谁执行呢?基于coroutineScope挂起特性我们无法从代码顺序去调整launch和coroutineScope.launch 的顺序。那么我们对于launch 设置挂起500毫秒。

fun main()= runBlocking {
    launch {
        delay(500)
        println("world")
        println("launch:"+Thread.currentThread().name)
    }
    coroutineScope {
        launch {
          //  delay(500)
            println("coroutineScope launch ")
        }
        //delay(2000)
        println("coroutineScope")
    }

    println("主线程:" +Thread.currentThread().name+"线程总量:"+Thread.activeCount())
    val threadGroup = Thread.currentThread().threadGroup
    val activeCount = Thread.activeCount()

    val threads = arrayOfNulls<Thread>(activeCount)
    threadGroup.enumerate(threads)
    threads.forEach {
        println("遍历的线程:"+ it?.name)
    }
}
// 结果 
coroutineScope
coroutineScope launch 
主线程:main线程总量:2
遍历的线程:main
遍历的线程:Monitor Ctrl-Break
world
launch:main

通过逻辑上可以发现,当coroutineScope执行完成后,便不会挂起协程了。那么launch便是由runBlocking管理。所以说,只要 coroutineScope 中的协程挂起或者耗时大于已有的协程,那么已有的协程 便会在coroutineScope生命周期内部处理,否则就会抛到外面。 那么它的意义是什么?

如果说,已有的协程比新coroutineScope 的协程耗时更短,就是直接coroutineScope中调度,否则就用原来的调度。 这玩意说明几个问题:协程是一个整体的框架,每一个协程都是统一调度的,只是说策略不一样。 使用coroutineScope便于作用域管理。

提取函数重构

使用suspend关键字。这种关键字标记的函数只能在协程中执行。添加这个关键字的函数可以执行挂起或者同步操作。这就避免了我们闭包的无限嵌套,通过这个关键字我们就可以在协程中写同步代码,而不需要处理回调。比如说网络请求,数据库读写,io读写等等。

suspend fun doWorld(){
    delay(300)
    println("world")
    println("launch:"+Thread.currentThread().name)
}

总结

写了这么多,主要是用于一种模板化的思路去理解协程,需要一个协程需要些什么?在后续的学习过程中才会理解其他特性。同时简述了下列知识点:

  • job 的都需要 context,有一个start
  • supend 关键字如何使用
  • GlobalScope.launch{} 简单协程。当然Android 不推荐使用这个,这个需要自己逻辑控制他。
  • runBlocking {} 阻塞线程
  • job.join() job的执行
  • coroutineScope {} 作用域构造器


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
FyeYl0ESQHUh