首页 > 其他分享 >kotlin协程的基础笔记

kotlin协程的基础笔记

时间:2023-10-05 20:32:26浏览次数:33  
标签:协程 Thread launch kotlin 笔记 线程 println main


导包

在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 {} 作用域构造器


标签:协程,Thread,launch,kotlin,笔记,线程,println,main
From: https://blog.51cto.com/u_16163453/7717152

相关文章

  • Android跨进程数据通道若干方案的实验笔记
    一、实验背景和目标我想做一个Android平台的跨进程数据通道,通过这个通道支持若干App之间的数据传输。我想到了一些传输方案,但是缺乏在方案中做出选型的评价依据。本实验会基于若干方案实现数据传输通道,在模拟的业务场景中进行实验,从功能性指标和非功能性指标对各方案做出评价。i.......
  • Logisim学习笔记
    教程计算机硬件系统设计(基于Logisim)-华中科技大学.谭志虎demo从零开始demo菜单->分析电路:(与Multisim有何不同?)todo......
  • 线段树学习笔记
    学习链接代码(未完成)#include<bits/std++.h>usingnamespacestd;intarray[200005],tree[200005<<2];//array是初始数组,tree是线段树voidupdate(intitem)//更新item号节点的函数,这里是最大值,也可更改为区间和、最小值等{tree[item]=max(tree[item<<1],tree[it......
  • 【Linux笔记】tar——压缩与解压
    #【Linux笔记】tar——压缩与解压打包与压缩打包文件(生成新的tar文件):tar-cfnewTar.tarfile.txt打包并压缩文件(生成新的.tar.gz文件):tar-zcfnewTar.tar.gzfile.txt注:打包和压缩是不一样的概念gzip这种压缩方式默认只能压缩一个文件,所以当有多个文件需要压缩时,就......
  • 微机原理笔记
    \[chapter1.\quad绪论\]Intel微处理器的发展1978年:8086/8088微处理器出现,首枚16位微处理器。微型计算机概述计算机加电以后,首先运行BIOS(BasicInputOutputSystem)系统,进行硬件的检查、初始化(加电时寄存器的内容是随机的)、给操作系统提供编程接口等。通过硬件驱动程......
  • 《需求掌握过程》阅读笔记
    今天读了《掌握需求过程·》这本书,理解了什么是需求,为什么要掌握需求,在开发软件时,身为一个程序员就要明白,开发软件的前前后后需要知道的东西,将尽可能多的可以预知的内容,做到心知肚明。目前的我们在开发软件的时候还是做的还是比较小的项目,偶尔也会遇到一些数据库设计出错导致,编写......
  • 数据库系统笔记
    \[Chapter1.\quad绪论\]数据库发展史人工管理阶段(1950)\(\Rightarrow\)文件系统阶段(1950-1960)\(\Rightarrow\)数据库系统阶段(1960-)数据库管理系统(DBMS)的出现,使得数据存储、数据管理和数据应用分离。数据库管理系统采用外模式-模式-内模式的三级模式,外模式/模式和模式/......
  • openGauss学习笔记-90 openGauss 数据库管理-内存优化表MOT管理-内存表特性-使用MOT-M
    openGauss学习笔记-90openGauss数据库管理-内存优化表MOT管理-内存表特性-使用MOT-MOT使用重试中止事务在乐观并发控制(OCC)中,在COMMIT阶段前的事务期间(使用任何隔离级别)不会对记录进行锁定。这是一个能显著提高性能的强大优势。它的缺点是,如果另一个会话尝试更新相同的记录,则更新......
  • Learning Hard C# 学习笔记: 3.C#语言基础
    前言由于最近工作开始重新使用了C#,框架也是.Net4.5,看了下,这本书是比较合适的,所以就重新学习了下,由于之前本人已有C#相关基础,所以不会所有内容都做笔记,只会对不熟悉或者比较重要的内容做笔记.3.2基础数据类型3.2.4枚举类型枚举类型属于值类型,用于定义一组命......
  • Learning Hard C# 学习笔记: 4.C#中的类
    类是面向对象语言都有的一种数据类型,它的存在在于将现实中的概念抽象概括为代码中的数据类型.4.1什么是类?以人类这个概念为例,人类就可以作为一个类,人类是一个种群,这个种群中包包含许多个体,这些个体可以当作一个对象.比如说小明就是人类中的一个个体,他是人类这个......