Kotlin 5. Kotlin 协程(Coroutines):globalScope,runBlocking,dispatcher,suspend,async,await,job

一起来学Kotlin:概念:5. Kotlin 协程(Coroutines):globalScope,runBlocking,dispatcher,suspend,async,await,job

本文将详细介绍 Kotlin Coroutines 中最重要的几个概念以及案例:globalScope,runBlocking,dispatcher,suspend,async,await,job。协程(Coroutines)是在 Kotlin 上进行异步编程的推荐解决方案(也是及其普遍的解决方案)。我们可以在单个线程上运行多个协程,其他操作不受影响。本文案例可直接在 Kotlin Playground 中运行。



1 GlobalScope

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 我们在接下来的章节中介绍。

2 RunBlocking

运行 runBlockingGlobalScope 不同。 它在运行后阻塞主线程,直到协程完成。 我们把上面的代码略微改动,即把 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 通常适用于单元测试的场景,而业务开发中不会用到这个函数,因为它是线程阻塞的。

3 NESTED COROUTINES:CoroutineScope

在 Android 应用程序中,我们在具有明确定义的生命周期的组件上实现 CoroutineScope。 这些组件包括 Activity、Fragment 和 ViewModel。

CoroutineScope 上调用 launch() 会提供一个封装了代码块的 Job。 一旦范围取消,所有相关的 Kotlin 协程都会清理它们的资源并取消。即,CoroutineScopelaunch()方法在不阻塞当前线程的情况下启动新的协程,并将协程的引用作为 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秒。
  • “5” 相当于 runBlocking 里面最后结束的,所以 “7” 在 “5” 之后结束。
  • 我们看到,“3”,“1”,“4” 几乎在同一时刻被打印,说明这三个携程几乎同一时刻被创建,创建后各做各的事情。
  • 我们在代码中注释掉了一行:job.cancel(),如果我们取消注释,那么,“5” 就不会被打印,原因就像上面说的,在 CoroutineScope 上调用 launch() 会提供一个封装了代码块的 Job。 一旦范围取消,所有相关的 Kotlin 协程都会清理它们的资源并取消。

注意看上面的代码,我们用到的是 coroutineScope,不是 CoroutineScope,因为 CoroutineScope 是一个 interface,而 coroutineScopekotlinx.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)
        }
    }
}

我们待会儿来解释什么是挂起函数,但在这里,我们先总结一下我们看到的:

  • launch 是 CoroutineScope 的一个扩展函数,该方法在不阻塞当前线程的情况下启动新的协程,launch 里面的代码会按顺序运行;
  • coroutineScope 是一个挂起函数,会挂起当前的协程。coroutineScope 里面的代码除了 launch,其他按照顺序运行;

在 Android 应用程序中取消协程有很多好处。 例如,假设一个应用程序进入后台并且一个 Activity 停止。 在这种情况下,我们应该取消任何长时间运行的 API 调用以清理资源。 这将帮助我们避免可能的内存泄漏或不需要的行为。比如说,我们可以从像 onStop() 这样的 Activity 事件中取消 Job 以及任何子项。

4 CoroutineDispatchers

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.Mainprivate val coroutineScope = CoroutineScope(Dispatchers.Main))。而到了读取图片的时候,使用的是Dispatchers.IOval imageLoad = coroutineScope.async(Dispatchers.IO) { getOriginalBitmap() })。最后进行图像处理的时候,使用的是Dispatchers.Defaultval 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)
}

5 Suspend 挂起函数

  • 挂起函数只能在协程或者其他挂起函数中调用(这就是为什么上面的例子中,首先需要通过在 coroutineScope 中 launch 一个新的协程,然后在这个协程中再进行调用挂起函数的原因)。
  • 挂起的对象是协程:launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被挂起,即,从正在执行它的线程上脱离。就是说,当前线程跳过这个挂起函数,继续往下运行,但另一方面,线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程,执行完后,返回到之前挂起它的线程;
  • 简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作;
  • 挂起函数的特点是使用同步的方式完成异步任务。
  • 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

有几点需要说明:

  • launch 是 CoroutineScope 的一个扩展函数,该方法在不阻塞当前线程的情况下启动新的协程,launch 里面的代码虽然有挂起函数,但还是会按顺序运行(注意,这里的挂起函数并没有用withContext选择去指定切换的线程);
  • coroutineScope 本身就是一个挂起函数,会挂起当前的协程。coroutineScope 里面的代码除了 launch,其他按照顺序运行,而 coroutineScope 里面可以 launch 多个 job,这多个 job 是并行的;
  • suspend 挂起函数里面的挂起函数是(默认)串行的(即,用同步的方式实现异步)。

6 async,await

我们先简化一下上面的那个案例(可直接在 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秒。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破浪会有时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值