Day7:CoroutineScope:launch() 以及 async()

在前一篇文章我们知道 suspend 函式必须要在 Coroutine scope 里面才能执行,本篇文章我们来了解一下两个 Coroutine Builder : launch()async()

launch()

如果一个 Coroutine 的回传值没有回传值,也就是回传 Unit 时,就可以使用 launch() 来建立一个 Coroutine。

使用范例:

fun main() = runBlocking {
    launch {
        println("launch Start")
        delay(500)
        println("launch End")
    }
    println("Done")
}

launch1

我们发现,这段程序码会先执行最外层的 println("Done") ,接着才是 launch() 里面的函式。

这是因为当使用 launch() 时,会建立一个新的 Coroutine,成为 runBlocking() 的子 Coroutine。如果在使用 launch() 时,没有带入 Coroutine context ,那麽预设会使用的调度器为 Dispatchers.Default ,也就是背景运算的 coroutine。所以就会出现 println("Done") 先执行的情况。

  • 那麽如果我们将 launch() 改为 coroutineScope() 会发生什麽事呢?
fun main() = runBlocking {
    coroutineScope {
            println("launch Start")
    delay(500)
    println("launch End")
    }
    println("Done")
}

Coroutine Scope

→ 结果则是按照原本的顺序来执行。因为利用 coroutineScope() 并不会建立新的 coroutine 而是继承外层的 coroutine context,也就是说,在 coroutineScope() 里面与外面的 println("Done") 其实都是在同一个 coroutine,所以会按照其顺序来执行。

launch() 里有三行程序,除了 delay(500) 是 suspend 函式以外,其他两行程序都是一般的程序码,所以我们可以将 launch() 内部的程序码重构,把这里面的程序码抽取出去。

suspend fun launchFun() {
    println("launch Start")
    delay(500)
    println("launch End")
}

因为这三行程序码有包含一行 suspend 函式 - delay() ,所以这个函式必须要加上 suspend 来修饰。

suspend 函式只能在 CoroutineScope 或是被另一个 suspend 函式调用。

Job

cancel()

launch() 的回传值是 JobJob 是代表一个可被取消的任务,我们可以呼叫 cancel() 取消该 coroutine 的执行。

如下:

fun main() = runBlocking {
    val job = launch {
        repeat(100_000) { index ->
            println("launch Start $index")
            delay(500)
            println("launch End $index")
        }
    }
    delay(1100)
    job.cancel()
    println("Done")
}

→ 在上面的范例中,我们在 launch 中执行了一段会执行十万次的任务,这个任务首先先印出 launch Start $index ,接着暂停该 Coroutine 500 毫秒,在 500 毫秒暂停时间结束之後,就会列印出 launch End $index

→ 我们把 launch 的回传值储存在 job 变数上,在外层的 coroutine 执行 1100 毫秒之後,就呼叫 job.cancel() 把十万次的任务停止。

那麽,会怎麽执行呢?

launch job

launch job

在这个范例中,Coroutine 的执行区块可以分成两块,红色区块我称为 「Coroutine1」,粉紫色区块我称为「Coroutine2」。我们可以看一下它执行的时间轴。

launch job timeline

Coroutine1 因为调用 delay(1100) ,所以 Coroutine1 暂停 1100 毫秒,在暂停之後调用了 Coroutine2 的 cancel() ,所以 Coroutine2 的任务被取消,当 Coroutine2 的任务被取消之後, Coroutine1 就能继续被执行。

join()

在前面的范例中,我们在 runBlocking 中建立一个 launch ,在 launch 所产生的 coroutine 就是在 runBlocking 里面的子 coroutine,所以当我们执行时,预设是会先执行外层的 coroutine,接着才是内层的 coroutine 。

所以下面的范例会先执行 println("Done") ,接着才会执行 launch 内部的任务。

fun main() = runBlocking {
    launch {
        println("launch Start")
        delay(500)
        println("launch End")
    }
    println("Done")
}

假如我们希望能够先完成 launch 里面的任务,完成之後我们才接续执行下面的任务,我们可以使用 job.join()

fun main() = runBlocking {
    val job = launch {
            println("launch Start")
    delay(500)
    println("launch End")
    }
    job.join()
    println("Done")
}

launch join

joinAll()

在 Kotlin 的 Coroutine 中,提供了 joinAll() 来同时针对多个 Job 来呼叫其 join()

fun main() = runBlocking {
    val job = launch {
        println("launch Start")
        delay(500)
        println("launch End")
    }
    joinAll(job)
    println("Done")
}

joinAll()

其实 joinAll() 只是呼叫带入 Job 的 join()

public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }

async()

await()

如同 launch()async() 也是用来建立 Coroutine 的,只不过与 launch() 不同的是, async() 是用来处理有回传值的非同步任务,而且它回传的是 Deferred 而不是 Job

fun main() = runBlocking{
    val deferred = async {
        println("async start")
        delay(500)
        println("async end")
        50
    }
    val deferredValue = deferred.await()
    println("done $deferredValue")
}

async await

我们将 async 回传的值存到变数 deferred 里面,这时候 async 里面的任务还没有开始执行,等到呼叫 await() 之後,就会执行 async ,并且在这边等待 async 完成,所以这边回传的值就会是 async 区块中的回传值,如上方范例的 50。

在 Kotlin 中,lambda 的最後一行就是它的回传值,我们可以省略 return ,不过如果要使用 return 也可以。

return@async 50

await() 的使用类似 join() 。前面有提到, join() 是会把该 Coroutine 任务完成之後,才会继续往下走, await() 的用法也是一样,当呼叫 await() 的时候,该 Coroutine 就会开始执行,直到结束或是发生例外。与 join() 不同的是, await() 是会回传在 async 区块的回传值,如上方的 50。

值得注意的是,我们在 async 并没有宣告回传的资料型别, Kotlin 会自动做型别推断。当然我们也可以自行加上型别。如下:

async<Int> {
    println("async start")
    delay(500)
    println("async end")
    50
}

不过 IDE 会提示你把它移除,因为 Kotlin 会自动型别推断。

如果没有使用 await() ?

async()launch() 一样,都是立刻被排程来执行,如果没有使用 await() ,在执行这段程序时,也会执行 async()

将范例的 await() 拿掉:

fun main() = runBlocking{
    async {
        println("async start")
        delay(500)
        println("async end")
        return@async 50
    }
    println("done")
}

async without await

→ 执行顺序就会跟使用 launch 一样。


延时任务

前面我们看了两个 coroutine builder : launch()async() ,我们知道当程序执行到这边的时候,就会将这两个 builder 所建造出来的 coroutine 排进执行的行程中。所以它们预设是立刻就被呼叫的。

那麽有没有一种方法可以让我们延後才执行呢?

有的,我们只需要在使用 launch()async() 时带入 CoroutineStart.LAZY 即可。

如下:

launch() 的延时执行

fun main() = runBlocking {
    launch(start = CoroutineStart.LAZY) {
            println("launch Start")
    delay(500)
    println("launch End")
    }
    println("Done")
}

→ 加上 CoroutineStart.LAZY 之後, launch() 里面的任务就不会立刻执行了。不过,如果没有启动 launch() 那麽程序就会在这边一直等它执行。

  • 使用 job.start() 来主动启动 Coroutine 的执行。
fun main() = runBlocking {
    val job = launch(start = CoroutineStart.LAZY) {
        println("launch Start")
        delay(500)
        println("launch End")
    }
    job.start()
    println("Done")
}

job.start

→ 这边我们也可以使用 job.join() 来启动。

async() 的延时任务

launch() 相同,我们也可以替 async() 加上 CoroutineStart.LAZY 来让 Coroutine 延後启动。

async() 可以使用 await()start() 或是 join() 来启动。

其中, await() 是有包含回传值得,其他两个没有。

小结

函式有分有回传值的以及没有回传值的, 当然 suspend 函式也有,Coroutine 提供了两种 Coroutine Builder 来处理这两种不同的 suspend 函式,没有回传值的对应的是 launch() Builder,而有回传值的对应的是 async() 。虽然这两个 Coroutine builder 回传的值不一样, launch() 回传的是 Job ,而 async() 回传的是 Deferred 。但是其实这两种回传值都本质上都是一样的,都是一个可以取消的背景任务。

JobDeferred 共同的函式有 cancel()start()join()

其中 cancel() 用来取消 coroutine, start() 用来启动 coroutine,而 join() 则是会让 coroutine 的任务完成之後,才把後面的工作加入。

因为 Deferred 是包含回传值的,所以我们可以使用 await() 来取得 coroutine scope 的回传值。

最後最後, launch() 以及 async() 都是在执行後立刻会被排进执行的顺序。如果想要延後才执行,就要在使用这两个函式的时候带入 CoroutineStart.LAZY。

心智图

CoroutineScope mind map

特别感谢

Kotlin Taiwan User Group
Kotlin 读书会


<<:  Day 10 : Docker基本操作 Volume篇

>>:  泳道图

Day 4. Hashicorp Nomad: resources

Hashicorp Nomad: resources 在Kubernets的 Quality of ...

[30天 Vue学好学满 DAY10] v-for 列表渲染

v-for 基於数组、物件透过迭代、遍历对前端进行渲染。 item in items items: ...

数位 AI 新时代

人的科技文明发展始终来自於人性 在数位的新时代浪潮席卷之下,世界各国不论是个人的发展,还是组织企业团...

day21: side effect

今天提到的是 side effect,就像之前提到的, side effect 通常出现在以下几种情...

[想试试看JavaScript ] HTML DOM

我们知道写程序有个阶段就是一个输入、运算处理、输出 网页是由HTML、CSS、Javascript三...