本文将详细介绍 Kotlin Coroutines 中最重要的几个概念以及案例:globalScope,runBlocking,dispatcher,suspend,async,await,job。协程(Coroutines)是在 Kotlin 上进行异步编程的推荐解决方案(也是及其普遍的解决方案)。我们可以在单个线程上运行多个协程,其他操作不受影响。本文案例可直接在 Kotlin Playground 中运行。
GlobalScope
不受任何 job
的约束。它用于启动在整个应用程序生命周期内运行的最高级协程(top level coroutines)。下面是一个显示如何使用 GlobalScope
的示例。注:代码都可以在 Kotlin Playground 中运行。
import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread
var dateTimeNow = ""
@OptIn(DelicateCoroutinesApi::class)
fun main(){
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code start: ${dateTimeNow}")
GlobalScope.launch {
delay(5000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("global scope: ${dateTimeNow}")
}
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code end: ${dateTimeNow}")
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
这里我们看到,在 GlobalScope.launch
的前后一行,我们各自打印了两句话。在 GlobalScope.launch
中间,我们进行了5秒的延迟处理。我们看到,打印出来的结果是:
code start: 20221008 09:41:33
code end: 20221008 09:41:33
也就是说,主线程的代码并没有因为 GlobalScope
中的 delay 而卡住。
另外需要说明的是,GlobalScope
一个 Delicate 的 API,在使用 GlobalScope
时很容易意外造成资源或内存泄漏。 在 GlobalScope
中启动的协程不受结构化并发原则的约束,因此如果由于问题(例如由于网络慢)而挂起或延迟,它将继续工作并消耗资源。例如,考虑以下代码:
fun loadConfiguration() {
GlobalScope.launch {
val config = fetchConfigFromServer() // network request
updateConfiguration(config)
}
}
对 loadConfiguration
的调用会在 GlobalScope
中创建一个协程,该协程在后台工作,无需任何取消或等待其完成的规定。如果网络很慢,它会一直在后台等待,消耗资源。重复调用 loadConfiguration
会消耗越来越多的资源。
所以说,在许多情况下,应避免使用 GlobalScope
,并且应将函数标记为 suspend
,例如:
suspend fun loadConfiguration() {
val config = fetchConfigFromServer() // network request
updateConfiguration(config)
}
如果 GlobalScope.launch
用于启动多个并发操作,则应使用 coroutineScope
:
// concurrently load configuration and data
suspend fun loadConfigurationAndData() {
coroutineScope {
launch { loadConfiguration() }
launch { loadData() }
}
}
关于 suspend
以及 coroutineScope
我们在接下来的章节中介绍。
运行 runBlocking
与 GlobalScope
不同。 它在运行后阻塞主线程,直到协程完成。 我们把上面的代码略微改动,即把 GlobalScope.launch
改成 runBlocking
:
import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread
var dateTimeNow = ""
@OptIn(DelicateCoroutinesApi::class)
fun main(){
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code start: ${dateTimeNow}")
runBlocking {
delay(5000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("runBlocking: ${dateTimeNow}")
}
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code end: ${dateTimeNow}")
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
打印结果如下:
code start: 20221008 10:05:10
runBlocking: 20221008 10:05:16
code end: 20221008 10:05:16
从打印的时间我们就能看到,runBlocking
运行后阻塞主线程。runBlocking
通常适用于单元测试的场景,而业务开发中不会用到这个函数,因为它是线程阻塞的。
在 Android 应用程序中,我们在具有明确定义的生命周期的组件上实现 CoroutineScope
。 这些组件包括 Activity、Fragment 和 ViewModel。
在 CoroutineScope
上调用 launch() 会提供一个封装了代码块的 Job。 一旦范围取消,所有相关的 Kotlin 协程都会清理它们的资源并取消。即,CoroutineScope
的launch()
方法在不阻塞当前线程的情况下启动新的协程,并将协程的引用作为 Job 返回。取消生成的 Job 时,协程将被取消。
import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread
var dateTimeNow = ""
fun main() = runBlocking { // this: CoroutineScope
launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("1 code start: ${dateTimeNow}")
delay(2000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("2 Task from runBlocking: ${dateTimeNow}")
}
coroutineScope { // Creates a new coroutine scope
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("3 coroutineScope created: ${dateTimeNow}")
val job = launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("4 Task from nested launch, this is printed: ${dateTimeNow}")
delay(5000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("5 Task from nested launch, this won't be printed: ${dateTimeNow}")
}
delay(1000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("6 Task from first coroutine scope: ${dateTimeNow}") // Printed before initial launch
//job.cancel() // This cancels nested launch's execution
}
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("7 Coroutine scope is over: ${dateTimeNow}") // This is not printed until nested launch completes/is cancelled
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
打印如下:
3 coroutineScope created: 20221008 10:38:54
1 code start: 20221008 10:38:54
4 Task from nested launch, this is printed: 20221008 10:38:54
6 Task from first coroutine scope: 20221008 10:38:55
2 Task from runBlocking: 20221008 10:38:56
5 Task from nested launch, this won't be printed: 20221008 10:38:59
7 Coroutine scope is over: 20221008 10:38:59
runBlocking
阻塞 Main 函数。所以,在 Main 函数下面那行的 launch
里面的代码是顺序运行的,所以这也是为什么 “1” 和 “2” 之间相差了2秒。coroutineScope
创建了一个新的 Scope,在 coroutineScope
里面的代码,除了 launch job 之外都是顺序运行的,所以这也是为什么 “3” 和 “6” 之间相差了1秒。coroutineScope
中,我们又 launch 了一个 job,我们看到 “4” 和 “5” 之间相差了5秒。runBlocking
里面最后结束的,所以 “7” 在 “5” 之后结束。job.cancel()
,如果我们取消注释,那么,“5” 就不会被打印,原因就像上面说的,在 CoroutineScope
上调用 launch() 会提供一个封装了代码块的 Job。 一旦范围取消,所有相关的 Kotlin 协程都会清理它们的资源并取消。注意看上面的代码,我们用到的是 coroutineScope
,不是 CoroutineScope
,因为 CoroutineScope
是一个 interface,而 coroutineScope
是 kotlinx.coroutines.CoroutineScope
下面的一个函数。我们来看一下 CoroutineScope.kt
中的源代码:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
也就是说,该函数被 suspend 修饰,是一个挂起函数。甚至,我们发现,delay 函数也是一个挂起函数:
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
我们待会儿来解释什么是挂起函数,但在这里,我们先总结一下我们看到的:
在 Android 应用程序中取消协程有很多好处。 例如,假设一个应用程序进入后台并且一个 Activity 停止。 在这种情况下,我们应该取消任何长时间运行的 API 调用以清理资源。 这将帮助我们避免可能的内存泄漏或不需要的行为。比如说,我们可以从像 onStop() 这样的 Activity 事件中取消 Job 以及任何子项。
Dispatchers
确定协程用于执行的线程或线程池。 Dispatchers
可以将协程限制在特定线程。它还可以将其分派到线程池。
这里是几种常用的:
Dispatchers.Main
,用于 Android 主线程。用于调用 suspend 函数,UI 框架操作,及更新 LiveData 对象。launch 在不加参数时,其默认值是 Dispatchers.Main
。Dispatchers.IO
:非主线程。用于磁盘操作(例如,Room 操作),及网络 I/O 请求(例如,调用服务器 API)。Dispatchers.Default
:非主线程,用于 CPU 密集型操作,就是说,需要花时间processing的。例如,list 排序,及 JSON 解析,图像处理计算等。一个例子如下:
class MainActivity : AppCompatActivity() {
private val IMAGE_URL = "https://i.kinja-img.com/gawker-media/image/upload/t_original/lj7y7c52vl96rjs4qjvx.jpg"
private val coroutineScope = CoroutineScope(Dispatchers.Main)
// Dispatchers.Main:Android 主线程。用于调用 suspend 函数,UI 框架操作,及更新 LiveData 对象。
// coroutine 将运行在主线程,而不是 I/O 专用的线程中。
// launch 在不加参数时,其默认值是 Dispatchers.Main。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
coroutineScope.launch {
// 这里我们先读取原图像
val imageLoad = coroutineScope.async(Dispatchers.IO) { getOriginalBitmap() }
// Dispatchers.IO:非主线程。用于磁盘操作(例如,Room 操作),及网络 I/O 请求(例如,调用服务器 API)。
// Here we call the function getOriginalBitmap on the IO dispatcher.
val originalBitmap: Bitmap = imageLoad.await()
// wait for the bitmap
loadOriginalImage(originalBitmap)
// 然后我们使用简单的图像处理将输入图像进行灰度。
val processedImageLoad = coroutineScope.async(Dispatchers.Default) { imageProcess(originalBitmap) }
// Dispatchers.Default 主要用于 CPU 密集型操作,就是说,需要花时间processing的。例如,list 排序,及 JSON 解析,图像处理计算等。
val filteredBitmap: Bitmap = processedImageLoad.await()
// wait for the bitmap
loadGrayImage(filteredBitmap)
}
}
private fun getOriginalBitmap() =
URL (IMAGE_URL).openStream().use {
BitmapFactory.decodeStream(it) // decode the stream into bitmap
}
private fun loadOriginalImage(bmp: Bitmap) {
imageView.setImageBitmap(bmp)
imageView.visibility = View.VISIBLE
}
private fun loadGrayImage(bmp: Bitmap) {
progressBar.visibility = View.GONE
imageView2.setImageBitmap(bmp)
imageView2.visibility = View.VISIBLE
}
private fun imageProcess(inputBitmap: Bitmap) = Filter.apply(inputBitmap)
}
我们一开始实例化 CoroutineScope 的时候使用的是 Dispatchers.Main
(private val coroutineScope = CoroutineScope(Dispatchers.Main)
)。而到了读取图片的时候,使用的是Dispatchers.IO
(val imageLoad = coroutineScope.async(Dispatchers.IO) { getOriginalBitmap() }
)。最后进行图像处理的时候,使用的是Dispatchers.Default
(val processedImageLoad = coroutineScope.async(Dispatchers.Default) { imageProcess(originalBitmap) }
)。
import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
var dateTimeNow = ""
fun main(){
var userName = ""
var userAge = 0
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("Main function starts. current time: ${dateTimeNow}")
runBlocking {
val downloadedName = async {
downloadName()
}
val downloadedAge = async {
downloadAge()
}
userName = downloadedName.await()
userAge = downloadedAge.await()
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("Main function ends: current time: ${dateTimeNow}")
println("${userName} ${userAge}")
}
}
suspend fun downloadName() : String {
delay(2000)
val userName = "Arif: "
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("username download. Current time: ${dateTimeNow}")
return userName
}
suspend fun downloadAge() : Int{
delay(4000)
val userAge = 28
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("user age download. Current time: ${dateTimeNow}")
return userAge
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
withContext
的作用就是指定切换的线程,比如:suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO)
。import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread
var dateTimeNow = ""
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking{
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code start: ${dateTimeNow}")
launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("1 code start: ${dateTimeNow}")
delay(2000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("2 Task from runBlocking: ${dateTimeNow}")
}
coroutineScope { // Creates a new coroutine scope
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("3 coroutineScope created: ${dateTimeNow}")
val job = launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("4 coroutineScope job starts: ${dateTimeNow}")
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("5 coroutineScope job ends: ${dateTimeNow}")
}
val job2 = launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("11 coroutineScope job2 starts: ${dateTimeNow}")
}
delay(1000L)
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("6 Task from first coroutine scope: ${dateTimeNow}") // Printed before initial launch
//job.cancel() // This cancels nested launch's execution
}
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code end: ${dateTimeNow}")
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
suspend fun doSomethingUsefulOne(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("7 第一个挂起函数开始: ${dateTimeNow}")
delay(1000L) // 假设我们在这里做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("8 第一个挂起函数结束: ${dateTimeNow}")
return 1
}
suspend fun doSomethingUsefulTwo(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("9 第二个挂起函数开始: ${dateTimeNow}")
delay(2000L) // 假设我们在这里也做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("10 第二个挂起函数结束: ${dateTimeNow}")
coroutineScope {
val job = launch {
doSomethingUsefulThree()
doSomethingUsefulFour()
}
}
return 2
}
suspend fun doSomethingUsefulThree(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("9 第三个挂起函数开始: ${dateTimeNow}")
delay(3000L) // 假设我们在这里也做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("10 第三个挂起函数结束: ${dateTimeNow}")
return 3
}
suspend fun doSomethingUsefulFour(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("9 第四个挂起函数开始: ${dateTimeNow}")
delay(3000L) // 假设我们在这里也做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("10 第四个挂起函数结束: ${dateTimeNow}")
return 4
}
打印的结果如下:
code start: 20221009 03:15:55
3 coroutineScope created: 20221009 03:15:55
1 code start: 20221009 03:15:55
4 coroutineScope job starts: 20221009 03:15:55
11 coroutineScope job2 starts: 20221009 03:15:55
7 第一个挂起函数开始: 20221009 03:15:55
6 Task from first coroutine scope: 20221009 03:15:56
8 第一个挂起函数结束: 20221009 03:15:56
9 第二个挂起函数开始: 20221009 03:15:56
2 Task from runBlocking: 20221009 03:15:57
10 第二个挂起函数结束: 20221009 03:15:58
9 第三个挂起函数开始: 20221009 03:15:58
10 第三个挂起函数结束: 20221009 03:16:01
9 第四个挂起函数开始: 20221009 03:16:01
10 第四个挂起函数结束: 20221009 03:16:04
5 coroutineScope job ends: 20221009 03:16:04
code end: 20221009 03:16:04
有几点需要说明:
我们先简化一下上面的那个案例(可直接在 Kotlin Playground 上运行看到结果):
import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread
var dateTimeNow = ""
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking{
coroutineScope { // Creates a new coroutine scope
val job = launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("4 coroutineScope job starts: ${dateTimeNow}")
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("5 coroutineScope job ends: ${dateTimeNow}, with result: ${one+two}")
}
}
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code end: ${dateTimeNow}")
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
suspend fun doSomethingUsefulOne(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("7 第一个挂起函数开始: ${dateTimeNow}")
delay(2000L) // 假设我们在这里做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("8 第一个挂起函数结束: ${dateTimeNow}")
return 1
}
suspend fun doSomethingUsefulTwo(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("9 第二个挂起函数开始: ${dateTimeNow}")
delay(3000L) // 假设我们在这里也做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("10 第二个挂起函数结束: ${dateTimeNow}")
return 2
}
结果打印如下:
4 coroutineScope job starts: 20221009 05:14:32
7 第一个挂起函数开始: 20221009 05:14:32
8 第一个挂起函数结束: 20221009 05:14:34
9 第二个挂起函数开始: 20221009 05:14:34
10 第二个挂起函数结束: 20221009 05:14:37
5 coroutineScope job ends: 20221009 05:14:37, with result: 3
code end: 20221009 05:14:37
也就是说,这个时候,两个挂起函数的运行是串联的,即,先运行完 doSomethingUsefulOne()
再运行 doSomethingUsefulOne()
。从结果上面的时间戳也能说明这个问题。
如果我们希望这两个挂起函数的运行是并联的,就可以用到 async。我们把上面的代码稍加改动,如下:
import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread
var dateTimeNow = ""
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking{
coroutineScope { // Creates a new coroutine scope
val job = launch {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("4 coroutineScope job starts: ${dateTimeNow}")
val oneDeferred = async {doSomethingUsefulOne()}
val twoDeferred = async {doSomethingUsefulTwo()}
val one = oneDeferred.await()
val two = twoDeferred.await()
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("5 coroutineScope job ends: ${dateTimeNow}, with result: ${one+two}")
}
}
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("code end: ${dateTimeNow}")
}
fun dateAsString(
dateInMillis: Long,
format: String = "yyyyMMdd HH:mm:ss",
locale: Locale = Locale.getDefault()
): String {
val date = Date(dateInMillis)
val formatter = SimpleDateFormat(format, locale)
return formatter.format(date)
}
suspend fun doSomethingUsefulOne(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("7 第一个挂起函数开始: ${dateTimeNow}")
delay(2000L) // 假设我们在这里做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("8 第一个挂起函数结束: ${dateTimeNow}")
return 1
}
suspend fun doSomethingUsefulTwo(): Int {
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("9 第二个挂起函数开始: ${dateTimeNow}")
delay(3000L) // 假设我们在这里也做了某些有用的工作
dateTimeNow = dateAsString(Calendar.getInstance().time.time)
println("10 第二个挂起函数结束: ${dateTimeNow}")
return 2
}
结果打印如下:
4 coroutineScope job starts: 20221009 05:18:18
7 第一个挂起函数开始: 20221009 05:18:18
9 第二个挂起函数开始: 20221009 05:18:18
8 第一个挂起函数结束: 20221009 05:18:20
10 第二个挂起函数结束: 20221009 05:18:21
5 coroutineScope job ends: 20221009 05:18:21, with result: 3
code end: 20221009 05:18:21
async 就好象 launch 一样. 它启动一个独立的协程, 也就是一个轻量的线程, 与其他所有协程一起并发执行. 区别在于, launch 返回 Job, 其中不带有结果值, 而 async 返回 Deferred,使用 await() 来得到它最终的计算结果。await() 在不阻塞线程的情况下等待该值的完成,并在延迟计算完成时恢复,返回结果值或如果延迟被取消则抛出相应的异常。
所以,不使用 async 的例子花了5秒运行完,而 async 的例子花了3秒。