Kotlin Coroutines - Basic usage of Coroutines

Basic usage of Kotlin coroutines

Kotlin coroutine series:

  • Basic use of coroutines (this article)
  • Contextual understanding of coroutines
  • Coroutine scope management
  • Common advanced use of coroutines

In fact, there are already a lot Kotlin协程of , and here I will publish one to record my own summary. It is also our own understanding and output.

Since the concept of coroutine is a relatively big thing, here I will break it down into several different modules to explain. This article is the first issue of the series. This series is about the practical use of each module of the coroutine. Most of it involves the principle and source code. This article talks about the basic use of coroutines.

You can see that I have prefixed the coroutines here, Kotlin协程, and the coroutines referred to later in the series are all Kotlin协程. It is already well known that Kotlin coroutines are implemented differently from coroutines in other languages. I won't introduce too much here, if you don't understand it, see here .

1. Why use coroutines

Android development uses the Java language, and Java's thread management is used Thread. We have used it more or less in Android development Thread.

But we need to update the UI in the main thread. After processing the logic in the sub-thread Thread, we need to call Api runOnUiThreadto switch to the main thread to update the UI, including Kotlin syntax. It is the same routine to use. Although there is an extension method of thread, the internal The processing flow is the same as in Java

The pseudo code is as follows:

   thread {
        dosth()

        runOnUiThread {
            updateUI()
        }
    }

If there is too much logic, or frequent thread switching, then even Kotlin's syntactic sugar will be nested many times, so we will use some excellent frameworks such as RxJavato manage some asynchronous and concurrent operations and manage switching threads logic.

而得益于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就是阻塞的,它会阻止其他协程的运行。

The same code, let's change GlobalScope.launch to runBlocking and try:

    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("执行完毕...")

       }

Result of running:

It can be seen that runBlocking really prevents other coroutines from running, and they have to run themselves before they can continue to run other coroutines. This is blocking.

Whether it is a scope or not, whether it is a sibling coroutine at the same level or a parent-child coroutine, it will be blocked.

        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("另一个协程执行完毕...")
        }

As shown in the above code, the coroutines of the two parent scopes are executed at the same time. Once the first coroutine is not blocked, the following coroutines can be executed. At this time, the two coroutines are concurrent, and the Log is as follows:

But if we modify the code to block the above coroutine, then the following coroutine needs to wait for the blocked code to be executed before it can be executed. At this time, the two coroutines are executed in non-concurrent serial order.

        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("另一个协程执行完毕...")
        }

The non-concurrent print results are as follows:

This explanation believes that everyone should be able to understand the concept of blocking.

Summarize

This article is the first in a series of coroutine usage. Basic usage. This article only covers basic concepts and basic usage. The above mentioned that coroutines have a life cycle and support cancellation and other features, which will be covered in the next few issues.

What we need to master in this issue are several ways to start coroutines, several ways to switch threads, the similarities and differences between asynchronous and synchronous execution, suspend functions, and the concepts of blocking and non-blocking.

The concept and framework of coroutines are relatively large. If I have any mistakes or mistakes in my explanation, I hope that students can point out and communicate.

If you feel that this article has inspired you a little, I hope you can 点赞support me. Your support is my biggest motivation.

Ok, this is the end of this issue.

I am participating in the recruitment of the creator signing program of the Nuggets Technology Community, click the link to register and submit .

Guess you like

Origin juejin.im/post/7117555477805793316