Kotlin Coroutines - Uso básico de Coroutines

Uso básico de las corrutinas de Kotlin

Serie de rutinas de Kotlin:

  • Uso básico de rutinas (este artículo)
  • Comprensión contextual de las rutinas
  • Gestión del alcance de la rutina
  • Uso avanzado común de rutinas

De hecho, ya hay un montón Kotlin协程de , y aquí publicaré uno para grabar mi propio resumen. También es nuestra propia comprensión y salida.

Dado que el concepto de rutina es relativamente amplio, aquí lo dividiré en varios módulos diferentes para explicarlo. Este artículo es el primer número de la serie. Esta serie trata sobre el uso práctico de cada módulo de la rutina. La mayor parte de involucra el principio y el código fuente.Este artículo habla sobre el uso básico de las corrutinas.

Puede ver que he agregado el prefijo a las corrutinas aquí, Kotlin协程y las corrutinas a las que se hace referencia más adelante en la serie son todas Kotlin协程. Ya es bien sabido que las corrutinas de Kotlin se implementan de manera diferente a las corrutinas en otros lenguajes. No introduciré demasiado aquí, si no lo entiende, vea aquí .

1. Por qué usar corrutinas

El desarrollo de Android utiliza el lenguaje Java y se utiliza la gestión de subprocesos de Java Thread. Lo hemos usado más o menos en el desarrollo de Android Thread.

Pero necesitamos actualizar la interfaz de usuario en el subproceso principal. Después de procesar la lógica en el subproceso Thread, debemos llamar a Api runOnUiThreadpara cambiar al subproceso principal para actualizar la interfaz de usuario, incluida la sintaxis de Kotlin. Es la misma rutina para usar. Aunque hay un método de extensión de subproceso, el flujo de procesamiento interno es el mismo que en Java

El pseudocódigo es el siguiente:

   thread {
        dosth()

        runOnUiThread {
            updateUI()
        }
    }

Si hay demasiada lógica o cambios frecuentes de subprocesos, incluso el azúcar sintáctico de Kotlin se anidará muchas veces, por lo que usaremos algunos marcos excelentes RxJavapara administrar algunas operaciones asincrónicas y concurrentes y administrar la lógica de subprocesos de conmutación.

而得益于Kotlin语言的设计,在1.3版本加入了协程的概念,后期又出了一些Jetpack的组件,天然支持协程,使得协程的概念越来越为人熟知,更多的人开始使用协程了。而我们也就无需使用 RxJava 等第三方框架来管理线程了。

协程的特点:

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 更少的内存泄漏:使用结构化并发机制在一个作用域内执行多项操作
  • 支持取消:取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发

二、怎么使用协程

Kotlin语法是默认不带协程的,如果我们想使用协程还是需要引入协程框架库。注意需要Kotlin版本1.3以上。这里我使用的Kotlin版本为1.4.21。

协程库的引入:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

我们就能使用一个简单的协程:

       GlobalScope.launch {
            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")
        }

Kotlin中,有几种方式能够启动协程,如下所示:

  • launch{} CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,并返回新协程的Job。

  • async{} CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,返回一个Deffer,除包装了未来的结果外,其余特性与launch{}一致

  • runBlocking{} 是一个裸方法,创建一个协程,并阻塞当前线程,直到协程执行完毕。前面说过,这里不再赘述。

  • withContext(){} 一个suspend方法,在给定的上下文执行给定挂起块并返回结果,它并不启动协程,只会(可能会)导致线程的切换。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。 withContext的目的不在于启动子协程,它最初用于将长耗时操作从UI线程切走,完事再切回来。

  • coroutineScope{} 一个suspend方法,创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解(即,将长耗时任务拆分为并发的多个短耗时任务,并等待所有并发任务完成后再返回)。

可以看到只有前三种方法是创建或启动一个协程的,后面那种方式都是切换线程,或者创建作用域的一个方法。

我们举例说明一下

    coroutineScope {  }  //报错 ,不能直接用,只能在协程里面使用
        runBlocking {
            coroutineScope {  }   //正常使用,因为runBlocking创建了协程
           
             YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")
        }
    runBlocking {
            coroutineScope {  //这里包一层也没什么,只是多了一层代码块而已, 不影响逻辑

            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")
            }
        }

源码查看

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

launch返回的是Job对象,用于控制协程的生命周期

异步的启动

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

async返回的是Deferred用于等待未来结果的返回。一般使用 await 来调用获取结果。

public suspend fun await(): T

切换线程withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 

内部需要传入一个协程上下文,我们一般使用调度器Dispatchers来切换线程,它是协程上下文CoroutineContext的实现类之一。(后面会单独出一期)

  • Dispatchers.Main Android主线程
  • Dispatchers.Unconfined 当前CoroutineScope的线程策略
  • Dispatchers.Default 默认值,为JVM共享线程池
  • Dispatchers.IO IO线程池,默认为64个线程

通过lauch 和 async 和 withContext 的使用示例如下:

     GlobalScope.launch{
            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            val deferred = async {
                YYLogUtils.w("切换到另一个协程")
                Thread.sleep(2000)
                return@async "response data"
            }

            val response = deferred.await()
            YYLogUtils.w("response:$response")


            val result = withContext(Dispatchers.IO) {
                //异步执行
                delay(1000L)
                YYLogUtils.w("切换到另一个协程")
                return@withContext "1234"
            }

            YYLogUtils.w("result:$result")

            delay(1000L)

            YYLogUtils.w("执行完毕...")

        }

这也是我们常用的两种方式:async是异步执行,withContext是同步执行。

当它们的代码块执行完毕,就会回到主协程的线程中,换句话说就是通过调度器实现切换线程,执行完就回到当前线程。如果想知道底层逻辑可以看这里

到这里就会简单的协程使用了,但是注意上面的代码有时候用的 sleep 有时候用的 delay ,看着意思都是延迟的意思,有什么区别?

三、协程的阻塞与挂起

阻塞的意思就是会阻断当前线程后面的代码不会执行。挂起的全名应该叫非阻塞式挂起,其意思是为不会阻塞其他协程,只是当前自己所在协程会挂起等待不执行,但是其他协程还是能继续执行的。

3.1 suspend非阻塞挂起函数

我们使用AS来编程,就很清晰的可以看到,左侧有箭头的就是挂起,而sleep方法是没有箭头的就不是挂起而是阻塞。

而挂起的方法调用都是需要 suspend 标记的,如

public suspend fun delay(timeMillis: Long) {
...
}

我们举一个简单的例子说明:

     GlobalScope.launch{
           YYLogUtils.w("执行在协程中...")

           val result1 = withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                return@withContext "1234"
            }

            YYLogUtils.w("result1:$result1")

            val result2 = withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                return@withContext "123456"
            }
            YYLogUtils.w("result2:$result2")

            YYLogUtils.w("执行完毕...")

        }

打印Log如下:

这是正常的,顺序执行的。为什么‘阻塞’了?不是说 delay 函数是挂起函数,是非阻塞的吗?OK,再次强调一点,此阻塞的概念并非是说阻塞这个线程,阻塞这段代码不让执行,此阻塞是针对其他 协程 的。上面的 withContext 它创建/启动了了协程吗?没有,它只是切换了线程,它本身其实也是 suspend 的函数而已。所以上面的代码是顺序执行的。

下面我们修改一下代码为启动协程:

     GlobalScope.launch{
           YYLogUtils.w("执行在协程中...")

           GlobalScope.launch(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            GlobalScope.launch(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }

            YYLogUtils.w("执行完毕...")

        }

挂起函数是不会阻塞协程的,打印Log如下:

而我们自定义的函数方法,也可以通过标记 suspend 而在协程中使用

GlobalScope.launch{
           YYLogUtils.w("执行在协程中...")

           saveSth2Local()

           val result1 = withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)

                saveSth2Local()

                return@withContext "1234"
            }

            YYLogUtils.w("result1:$result1")

           

            YYLogUtils.w("执行完毕...")

        }

   suspend fun saveSth2Local() {
        DBHelper.get().saveUser()
    }       

使用我们定义的 saveSth2Local 挂起方法的时候,在哪个作用域使用就是在哪个线程执行,如上面的 saveSth2Local 方法是在主线程执行,withContext 中的 saveSth2Local 方法则是在子线程中使用。

3.2 runBlocking阻塞协程

上面我们讲到的是 suspend 挂起函数的阻塞与非阻塞的概念,而我们启动函数launch 和 runBlocking 也是区分阻塞与非阻塞的。概念都是一样的,就是是否阻塞其他协程。

launch的是非阻塞的,runBlocking就是阻塞的,它会阻止其他协程的运行。

El mismo código, cambiemos GlobalScope.launch a runBlocking e intentemos:

    GlobalScope.launch{
          YYLogUtils.w("执行在协程中...")

          runBlocking(Dispatchers.IO) {
               //异步执行
               YYLogUtils.w("异步执行result1")
               delay(1000L)
               YYLogUtils.w("result1:1234")
           }

           runBlocking(Dispatchers.IO) {
               //异步执行
               YYLogUtils.w("异步执行result2")
               delay(1000L)
               YYLogUtils.w("result2:123456")
           }

           YYLogUtils.w("执行完毕...")

       }

Resultado de correr:

Se puede ver que runBlocking realmente evita que se ejecuten otras corrutinas, y tienen que ejecutarse ellas mismas antes de poder continuar ejecutando otras corrutinas. Esto es bloquear.

Ya sea que se trate de un alcance o no, ya sea una corrutina hermana en el mismo nivel o una corrutina padre-hijo, se bloqueará.

        CoroutineScope(Dispatchers.Main).launch {
            YYLogUtils.w("执行在协程中...")

            withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }

            delay(1000L)

            YYLogUtils.w("执行完毕...")

        }


        GlobalScope.launch(Dispatchers.Main) {
            YYLogUtils.w("执行在另一个协程中...")

            delay(1000L)

            YYLogUtils.w("另一个协程执行完毕...")
        }

Como en el código anterior, las corrutinas de los dos ámbitos principales se ejecutan al mismo tiempo. Una vez que la primera corrutina no está bloqueada, se pueden ejecutar las siguientes corrutinas. En este momento, las dos corrutinas son concurrentes y el registro es como sigue:

Pero si modificamos el código para bloquear la corrutina anterior, entonces la siguiente corrutina debe esperar a que se ejecute el código bloqueado antes de poder ejecutarse.En este momento, las dos corrutinas se ejecutan en orden de serie no concurrente.

        CoroutineScope(Dispatchers.Main).launch {
            YYLogUtils.w("执行在协程中...")

            runBlocking(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            runBlocking(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }

            delay(1000L)

            YYLogUtils.w("执行完毕...")

        }


        GlobalScope.launch(Dispatchers.Main) {
            YYLogUtils.w("执行在另一个协程中...")

            delay(1000L)

            YYLogUtils.w("另一个协程执行完毕...")
        }

Los resultados de la impresión no concurrente son los siguientes:

Esta explicación cree que todos deberían poder entender el concepto de bloqueo.

Resumir

Este artículo es el primero de una serie sobre el uso de las corrutinas. Uso básico. Este artículo solo incluye los conceptos básicos y el uso básico. Lo anterior menciona que las corrutinas tienen un ciclo de vida, admiten la cancelación y otras características, que se tratarán en el próximo pocos problemas

Lo que debemos dominar en este problema son varias formas de iniciar corrutinas, varias formas de cambiar hilos, las similitudes y diferencias entre la ejecución asíncrona y síncrona, las funciones de suspensión y los conceptos de bloqueo y no bloqueo.

El concepto y el marco de las corrutinas son relativamente amplios. Si tengo algún error o error en mi explicación, espero que los estudiantes puedan señalarlo y comunicarlo.

Si sientes que este artículo te ha inspirado un poco, espero que puedas 点赞apoyarme, tu apoyo es mi mayor motivación.

Ok, este es el final de este problema.

Estoy participando en el reclutamiento del programa de firma de creadores de la Comunidad Tecnológica de Nuggets, haga clic en el enlace para registrarse y enviar .

Supongo que te gusta

Origin juejin.im/post/7117555477805793316
Recomendado
Clasificación