【Kotlin精简】第8章 协程

1 简介

Kotlin 中的协程提供了一种全新处理并发的方式,您可以在 Android 平台上使用它来简化异步执行的代码。协程是从 Kotlin 1.3 版本开始引入,但这一概念在编程世界诞生的黎明之际就有了,最早使用协程的编程语言可以追溯到 1967 年的 Simula 语言。

在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 JavascriptC#PythonRuby 以及 Go 等。Kotlin协程是基于来自其他语言的既定概念。

Android 平台上,协程主要用来解决两个问题:

  1. 处理耗时任务 (Long running tasks),这种任务常常会阻塞住主线程
  2. 保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数

特点一句话总结:协程能更加安全实现异步代码同步化,实质是对线程切换的封装

在这里插入图片描述

2 Kotlin协程创建

下面我们来看看创建协程的三种方式:

2.1 使用 runBlocking 顶层函数创建

    fun runBlockingTest(){
        runBlocking {
            KyLog.i(
                "yvan","runBlocking"
            )
        }
    }

2.2 使用 GlobalScope 单例对象创建

    fun globalScopeTest(){
        GlobalScope.launch {
            Log.i(
                "yvan","GlobalScope launch"
            )
        }
    }

2.3 自行通过 CoroutineContext 创建一个 CoroutineScope 对象

    fun coroutineScopeTest(){
        val coroutineScope = CoroutineScope(Dispatchers.IO)
        coroutineScope.launch {
            KyLog.i(
                "yvan","CoroutineScope launch"
            )
        }
    }

2.4 使用总结

  1. 方法一runBlocking通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的,不推荐
  2. 方法二GlobalScope和使用方法一runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。
  3. 方法三CoroutineContext是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 contextAndroid 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

3 Kotlin协程取消

与线程类比,Java 线程其实没有提供任何机制来安全地终止线程。
Thread 类提供了一个方法 interrupt() 方法,用于中断线程的执行。调用interrupt()方法并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息,然后由线程在下一个合适的时机中断自己。协程Job 接口有一个 cancel() 方法,用于取消它,调用它会触发以下效果:

  1. 协程会在第一个挂起点结束 job (下面例子中的 delay)
  2. 如果一个 job 有几个子 job,它们也会被取消(但是它的父 job 不受影响)
  3. 一旦一个 job 被取消,它就不能被用作任何新 job 的父 job。它首先处于 “Cancelling” 状态,然后处于 “Cancelled” 状态

3.1 job的cancel()、join()、cancelAndJoin()方法

3.1.1 job的cancel()

取消之后,我们通常会调用 join() 方法,程序必须要等到“取消”执行完才能继续。如果没有这个函数,我们可能就会有一些别的竞争。
下面代码展示了一个示例,在IO线程没有调用 join() 的情况下,我们将会看到 “repeat end 0” 在 “Cancelled” 后面:

CoroutineScope(Dispatchers.IO).launch {
	Log.i(
	    "yvan", "CoroutineScope launch"
	)
	onlyCancel()
}
        
private suspend fun onlyCancel() = coroutineScope {
    val job = launch {
        repeat(200) { i ->
            Log.i("yvan", "repeat start $i thread:${Thread.currentThread().name}")
            delay(100)
            Log.i("yvan", "repeat doing $i")
            Thread.sleep(100) // 我们模拟一些耗时操作
            Log.i("yvan", "repeat end $i")
        }
    }
    delay(200)
    job.cancel()
    Log.i("yvan", "Cancelled")
}

上面的打印结果:

yvan: CoroutineScope launch
yvan: repeat start 0 thread:DefaultDispatcher-worker-3
yvan: repeat doing 0
yvan: Cancelled
yvan: repeat end 0
yvan: repeat start 1 thread:DefaultDispatcher-worker-1

3.1.2 job的join()

cancel()之后,先往后执行Cancelled后还能继续执行repeat()方法内的逻辑,加上 job.join() 将会改变这一点, 因为它会挂起,直到一个协程完成取消。

CoroutineScope(Dispatchers.IO).launch {
	Log.i(
	    "yvan", "CoroutineScope launch"
	)
	cancelAndJoin()
}

private suspend fun cancelAndJoin() = coroutineScope {
    val job = launch {
        repeat(200) { i ->
            Log.i("yvan", "repeat start $i thread:${Thread.currentThread().name}")
            delay(100)
            Log.i("yvan", "repeat doing $i")
            Thread.sleep(100) // 我们模拟一些耗时操作
            Log.i("yvan", "repeat end $i")
        }
    }
    delay(200)
    job.cancel()
    job.join()
    // 为了更容易地同时调用 cancel() 和 join(), kotlinx.coroutines 提供了更方便的扩展函数: cancelAndJoin()。
    // job.cancelAndJoin()
    Log.i("yvan", "Cancelled")
}

加上 job.join() 的打印结果:

yvan: CoroutineScope launch
yvan: repeat start 0 thread:DefaultDispatcher-worker-3
yvan: repeat doing 0
yvan: repeat end 0
yvan: repeat start 1 thread:DefaultDispatcher-worker-3
yvan: Cancelled

加上job.join()的打印结果是执行完repeat()内所有逻辑才往后执行Cancelled
需要注意:上面是IO线程的情况,如果在Main线程,则不管是否有job.join(),打印结果都跟IO线程加上job.join()的顺序一致。

因为取消发生在挂起点上,如果没有挂起点就不会发生。为了模拟这种情况,我们使用了 Thread.sleep 而不是 delay 这种做法不太好,所以请不要在任何现实项目中这么做。我们只是试图模拟一种情况,在这种情况下,我们广泛的使用我们的协程,但没有挂起它们。在实践中,如果我们有一些更复杂的计算,比如神经网络学习(是的,为了简化处理并行化,我们也会使用协程),或者当我们需要做一些阻塞调用(例如,读取文件)时,就会发生这种情况。

3.1.3 job一次性取消多个协程

使用 Job() 工厂函数创建的 job 可以以同样的方式被取消。这通常用于一次性取消多个协程

    CoroutineScope(Dispatchers.IO).launch {
        Log.i(
            "yvan", "CoroutineScope launch"
        )
        jobFactory()
    }
        
    private suspend fun jobFactory(): Unit = coroutineScope {
        val job = Job()
        launch(job) {
            repeat(400) { i ->
                delay(200)
                Log.i("yvan", "job1 repeat $i thread:${Thread.currentThread().name}")
            }
        }
        launch(job) {
            repeat(400) { i ->
                delay(200)
                Log.i("yvan", "job2 repeat $i thread:${Thread.currentThread().name}")
            }
        }
        delay(400)
        job.cancelAndJoin()
        Log.i("yvan", "Cancelled")
    }

打印结果

yvan: CoroutineScope launch
yvan: job2 repeat 0 thread:DefaultDispatcher-worker-2
yvan: job1 repeat 0 thread:DefaultDispatcher-worker-3
yvan: Cancelled

Job() 一次性取消多个协程这个能力比较重要。我们经常需要取消一组并发任务。例如,在 Android 中,当用户离开一个视图时,我们需要取消此视图启动的多个协程。

3.2 CancellationException 异常

3.2.1 异常捕获及finally块

当一个 job 被取消时,它的状态变成 Cancelling,然后,在第一个挂起点,抛出一个 CancellationException 异常。可以使用 try-catch 来捕获这个异常。

    private suspend fun tryCatchCancelAndJoin(): Unit = coroutineScope {
        val job = Job()
        launch(job) {
            try {
                repeat(400) { i ->
                    delay(200)
                    Log.i("yvan", "job repeat $i thread:${Thread.currentThread().name}")
                }
            } catch (e: CancellationException) {
                Log.i("yvan", "job repeat error $e")
            } finally {
                Log.i("yvan", "job repeat finally deal")
            }
        }
        delay(400)
        job.cancelAndJoin()
        Log.i("yvan", "Cancelled")
    }

打印结果:

yvan: CoroutineScope launch
yvan: job repeat 0 thread:DefaultDispatcher-worker-3
yvan: job repeat error kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@ccbaa8c
yvan: job repeat finally deal
yvan: Cancelled

一个被取消的协程不是仅仅的停止:它是使用一个异常在内部取消的。因此,我们可以自由地在 finlay 块清理所有的东西。例如,我们可以使用 finally 块来关闭文件数据库连接等。

3.2.2 finally块中再次使用协程

由于我们可以捕获 CancellationException ,在协程真正结束之前可以执行一些操作,你可能想知道有没有什么限制。只要需要清理所有资源,协程就可以运行。然而,挂起是不允许的。 job 已经处于 “Cancelling” 状态,在这种状态下,挂起或启动另一个协程是不可能的。如果我们启动另一个协程,它将被忽略,如果我们尝试挂起,它将会抛出 CancellationException

   private suspend fun tryCatchCancelAndJoin(): Unit = coroutineScope {
        val job = Job()
        launch(job) {
            try {
                repeat(400) { i ->
                    delay(200)
                    Log.i("yvan", "job repeat $i thread:${Thread.currentThread().name}")
                }
            } catch (e: CancellationException) {
                Log.i("yvan", "job repeat error $e")
            } finally {
                Log.i("yvan", "job repeat finally deal")
                launch { 
                    // 这个launch内部会被忽略,不执行
                    Log.i("yvan", "job repeat finally launch")
                }
                try {
                    delay(400) // 会抛出异常
                } catch (e: Exception) {
                    Log.i("yvan", "job repeat error2 $e")
                }
  
                Log.i("yvan", "job repeat finally end")
            }
        }
        delay(400)
        job.cancelAndJoin()
        Log.i("yvan", "Cancelled")
    }

打印结果:

yvan: CoroutineScope launch
yvan: job repeat 0 thread:DefaultDispatcher-worker-3
yvan: job repeat error kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@ccbaa8c
yvan: job repeat finally deal
yvan: job repeat error2 kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@ccbaa8c
yvan: job repeat finally end
yvan: Cancelled

job 已经处于 “Cancelling” 状态下,finally中的再次使用协程launch内不会再执行。

3.2.3 不能被取消的 job

有时,当协程已经取消时,我们确实需要使用挂起函数。在这种情况下,首选的方法是使用 withContext(NonCancellable) 函数来包装这个调用。在 withContext 中,我们使用了 NonCancelable 对象,这是一个不能被取消的 job。因此,在 block 代码块中,job 处于活跃状态,我们可以调用任何我们想要的挂起函数。

    CoroutineScope(Dispatchers.IO).launch {
        Log.i(
            "yvan", "CoroutineScope launch"
        )
        tryCatchCancelAndJoinNonCancellable()
    }
        
	private suspend fun tryCatchCancelAndJoinNonCancellable(): Unit = coroutineScope {
	      val job = Job()
	      launch(job) {
	          try {
	              delay(200)
	              Log.i("yvan", "job finished")
	          } catch (e: CancellationException) {
	              Log.i("yvan", "job catch $e")
	          } finally {
	              Log.i("yvan", "job finally")
	              withContext(NonCancellable) {
	                  delay(200)
	                  Log.i("yvan", "job cleanup done")
	              }
	          }
	      }
	      delay(100)
	      job.cancelAndJoin()
	      Log.i("yvan", "job done")
	  }

打印结果:

yvan: CoroutineScope launch
yvan: job catch kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@ccbaa8c
yvan: job finally
yvan: job cleanup done
yvan: job done

3.3 invokeOnCompletion

Job 中提供了释放资源机制的 invokeOnCompletion 函数。它用于设置当 job 到达最终状态时(即 “Completed” 或 “Cancelled”)回调的代码。

    CoroutineScope(Dispatchers.IO).launch {
        Log.i(
            "yvan", "CoroutineScope launch"
        )
        invokeOnCompletion()
    }
        
    private suspend fun invokeOnCompletion(): Unit = coroutineScope {
        val job = launch {
            delay(400)
            Log.i("yvan", "job launch start")
            delay(100)
            Log.i("yvan", "job launch end")
        }
        job.invokeOnCompletion { exception: Throwable? ->
            Log.i("yvan", "Finished exception:$exception")
        }
        delay(400)
        job.cancelAndJoin()
        Log.i("yvan", "job done")
    }

打印结果:

yvan: CoroutineScope launch
yvan: job launch start
yvan: Finished exception:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@ccbaa8c
yvan: job done

这个回调函数的参数exception是一个异常:

  1. 协程完成或没有异常为null
  2. 协程被取消为CancellationException
  3. 如果协程异常则为对应的Exception

job 在调用 invokeOnCompletion 之前已经完成,那么回调函数将立即被调用。

下面的例子展示了一种情况,协程不能取消,因为它里面没有挂起点(我们使用 Thread.sleep 而不是 delay)。即便它应该在400毫秒后取消,但实际上执行超过了1分钟。

	CoroutineScope(Dispatchers.IO).launch {
		Log.i(
		   "yvan", "CoroutineScope launch"
		)
		nonCancel()
	}
        
    private suspend fun nonCancel(): Unit = coroutineScope {
        val job = Job()
        launch(job) {
            repeat(400) { i ->
                Thread.sleep(200)
                // 这里我们可能有一些复杂的操作,例如读取文件
                Log.i("yvan", "repeat $i thread:${Thread.currentThread().name}")
            }
        }
        delay(400)
        job.cancelAndJoin()
        Log.i("yvan", "Cancelled")
        delay(400)
    }

打印结果:

yvan: CoroutineScope launch
yvan: repeat 0 thread:DefaultDispatcher-worker-3
yvan: repeat 1 thread:DefaultDispatcher-worker-3

yvan: repeat 399 thread:DefaultDispatcher-worker-3
yvan: Cancelled

repeat()中times为400ms即执行了400次,每次sleep200ms执行,所以总的时间为80000ms。

3.4 isActive

我们可以使用 isActive 属性来检查 job 是否仍然处于活跃状态,并在 job 处于非活跃状态时停止计算。

	CoroutineScope(Dispatchers.IO).launch {
	   Log.i(
	          "yvan", "CoroutineScope launch"
	      )
	   nonCancelActive()
	}
        
    private suspend fun nonCancelActive(): Unit = coroutineScope {
        val job = Job()
        launch(job) {
            var count = 0
            do {
                Thread.sleep(200)
                count++
                Log.i("yvan", "while $count thread:${Thread.currentThread().name}")
                // 通过isActive限制继续执行
            } while (isActive)
        }
        delay(500)
        job.cancelAndJoin()
        Log.i("yvan", "Cancelled")
    }

打印结果:

yvan: CoroutineScope launch
yvan: while 1 thread:DefaultDispatcher-worker-3
yvan: while 2 thread:DefaultDispatcher-worker-3
yvan: while 3 thread:DefaultDispatcher-worker-3
yvan: Cancelled

3.5 ensureActive

我们也可以使用 ensureActive() 函数,它会在 Job 不活跃时候抛出 CancelllationException

    CoroutineScope(Dispatchers.IO).launch {
     	Log.i(
            "yvan", "CoroutineScope launch"
        )
        nonCancelEnsureActive()
    }
    

    private suspend fun nonCancelEnsureActive(): Unit = coroutineScope {
        val job = Job()
        launch(job) {
            try {
                repeat(400) { num ->
                    Thread.sleep(200)
                    // 协程被取消后,会导致抛出CancelllationException异常
                    ensureActive()
                    Log.i("yvan", "repeat $num thread:${Thread.currentThread().name}")
                }
            } catch (e: Exception) {
                Log.i("yvan", "repeat catch $e")
            }
        }
        delay(500)
        job.cancelAndJoin()
        Log.i("yvan", "Cancelled")
    }

打印结果:

yvan: CoroutineScope launch
yvan: repeat 0 thread:DefaultDispatcher-worker-3
yvan: repeat 1 thread:DefaultDispatcher-worker-3
yvan: repeat catch kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@ccbaa8c
yvan: Cancelled

yield()ensureActive 使用方式一样。
yield 会进行的第一个工作就是检查任务是否完成,如果 Job 已经完成的话,就会抛出 CancellationException 来结束协程。yield 应该在定时检查中最先被调用。

ensureActive()yield() 的结果看起来十分相似,但它们有很大的不同。函数 ensureActive() 需要在 CoroutinScopeCoroutineContextJob作用域内调用。它所做的事情只是job 不再活跃时抛出异常。它更轻量,所以通常它应该是首选。函数 yield 是一个常规的顶层挂起函数。它不需要任何作用域,因此可以在任意常规挂起函数中使用。由于它执行挂起和恢复操作,因此可能会产生其它影响,例如,如果我们使用带有线程池的分发器,则会导致线程更改。 yield 通常只用于挂起 CPU 密集型或阻塞线程的函数。

3.6 suspendCancellableCoroutine

suspendCancellableCoroutine它的行为类似于 suspendCoroutine,但是它的 continuation 被包装到了提供了额外方法的 CancellableContinuation<T> 中。最重要的一个方法是 invokeOnCancellation,我们使用它来定义取消协程时应该发生什么。我们通常使用它来取消库中的进程或者释放一些资源。

suspend fun someTask() = suspendCancellableCoroutine { cont ->
    cont.invokeOnCancellation {
        // do cleanup
    }
    // rest of the implementation
}

CancellableContinuation<T> 也允许我们检查 job 的状态(通过使用 isActiveisCompletedisCancelled 属性),并使用可选的取消原因(异常)取消这个 continuation

3.7 协程取消总结

取消是一个强大的功能。它通常很容易使用,但有时会很棘手。所以,了解它的工作原理很重要。
正确使用取消操作意味着更少的资源浪费和更少的内存泄漏,这对我们的应用程序的性能很重要。

4 等待协程执行

  1. 无返回值的协程使用 launch 函数创建
  2. 需要返回值,则通过 async 函数创建。

使用 async 方法启动 Deferred (也是一种 job), 可以调用它的 await() 方法获取执行的结果

    private suspend fun asyncTest(): Unit = coroutineScope {
        val asyncDeferred = async {
            // do some
        }

        // 等待结果返回
        val result = asyncDeferred.await()
    }
  1. deferred 也是可以取消的,对于已经取消的 deferred 调用 await() 方法,会抛出JobCancellationException 异常。
  2. deferred.await 之后调用 deferred.cancel(),那么什么都不会发生,因为任务已经结束了。

5 协程异常处理

上面协程取消中已经提到过,挂起函数包裹在 try/catch 代码块中,这样就可以在 finally 代码块中进行资源清理等操作了,具体请看3.2

6 协程超时

绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动,使用 withTimeout 函数。

  	CoroutineScope(Dispatchers.IO).launch {
        Log.i(
            "yvan", "CoroutineScope launch"
        )
        withTimeoutTest()
    }


    private suspend fun withTimeoutTest(): Unit = coroutineScope {
        val result = withTimeout(300) {
            try {
                Log.i("yvan", "start")
                delay(100)
                Log.i("yvan", "1")
                delay(100)
                Log.i("yvan", "2")
                delay(100)
                Log.i("yvan", "3")
                delay(100)
                Log.i("yvan", "4")
                delay(100)
                Log.i("yvan", "5")
                Log.i("yvan", "end")
            } catch (e: Exception) {
                Log.i("yvan", "e:$e")
            }
        }
        Log.i("yvan", "result:$result")
    }

打印结果:

yvan: CoroutineScope launch
yvan: start
yvan: 1
yvan: 2
yvan: e:kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms

withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。
当然,还有另一种方式: 使用 withTimeoutOrNull,两个函数正常执行完后都有返回值,但两者的区别在于:

  1. withTimeout 超时则无返回值,直接抛出一个超时异常 TimeoutCancellationException
  2. withTimeoutOrNull 函数会在超时也会有返回一个 null

7 协程并发与挂起

7.1 async实现并发

考虑一个场景: 开启多个任务,并发执行,所有任务执行完之后,返回结果,再汇总结果继续往下执行。

针对这种场景,解决方案有很多,比如 JavaFeatureTaskconcurrent 包里面的 CountDownLatchSemaphoreRxjava 提供的 Zip 变换操作等。

前面提到有返回值的协程,我们通常使用 async 函数来启动。

  private fun asyncTime() = runBlocking {
        val time = measureTimeMillis {
            val a = async(Dispatchers.IO) {
                Log.i("yvan", "async1 thread:${Thread.currentThread().name}")
                delay(1000) // 模拟耗时操作
                1
            }
            val b = async(Dispatchers.IO) {
                Log.i("yvan", "async2 thread:${Thread.currentThread().name}")
                delay(2000) // 模拟耗时操作
                2
            }
            Log.i("yvan", "a+b=${a.await() + b.await()}")
            Log.i("yvan", "end")
        }
        Log.i("yvan", "time: $time")
    }

打印结果:

15:38:17.260 6043-6083/com.example.kotlin I/yvan: CoroutineScope launch
15:38:17.261 6043-6085/com.example.kotlin I/yvan: async1 thread:DefaultDispatcher-worker-3
15:38:17.262 6043-6070/com.example.kotlin I/yvan: async2 thread:DefaultDispatcher-worker-1
15:38:19.266 6043-6083/com.example.kotlin I/yvan: a+b=3
15:38:19.266 6043-6083/com.example.kotlin I/yvan: end
15:38:19.266 6043-6083/com.example.kotlin I/yvan: time: 2006

async 启动一个协程后,调用 await 方法后,会阻塞,等待结果的返回,同样能达到效果。

7.2 async实现惰性启动

async 可以通过将 start 参数设置为 CoroutineStart.LAZY 变成惰性的。在这个模式下,调用 await 获取协程执行结果的时候,或者调用 Jobstart 方法时,协程才会启动

   private fun asyncTime2() = runBlocking {
        val time = measureTimeMillis {
            val a = async(Dispatchers.IO, CoroutineStart.LAZY) {
                Log.i("yvan", "async1 thread:${Thread.currentThread().name}")
                delay(1000) // 模拟耗时操作
                1
            }
            val b = async(Dispatchers.IO, CoroutineStart.LAZY) {
                Log.i("yvan", "async2 thread:${Thread.currentThread().name}")
                delay(2000) // 模拟耗时操作
                2
            }
            a.start()
            b.start()
            Log.i("yvan", "a+b=${a.await() + b.await()}")
            Log.i("yvan", "end")
        }
        Log.i("yvan", "time: $time")
    }

打印结果:

15:42:48.796 6460-6489/com.example.kotlin I/yvan: CoroutineScope launch
15:42:48.799 6460-6491/com.example.kotlin I/yvan: async1 thread:DefaultDispatcher-worker-3
15:42:48.799 6460-6490/com.example.kotlin I/yvan: async2 thread:DefaultDispatcher-worker-2
15:42:50.803 6460-6489/com.example.kotlin I/yvan: a+b=3
15:42:50.804 6460-6489/com.example.kotlin I/yvan: end
15:42:50.804 6460-6489/com.example.kotlin I/yvan: time: 2007

如果上面的start不调用,依靠await方法启动,则需要等到a.await后1000ms才能执行b.await,b再执行2000ms后才能输出。
打印结果:

15:42:58.760 6542-6569/com.example.kotlin I/yvan: CoroutineScope launch
15:42:58.762 6542-6571/com.example.kotlin I/yvan: async1 thread:DefaultDispatcher-worker-3
15:42:59.766 6542-6571/com.example.kotlin I/yvan: async2 thread:DefaultDispatcher-worker-3
15:43:01.770 6542-6569/com.example.kotlin I/yvan: a+b=3
15:43:01.770 6542-6569/com.example.kotlin I/yvan: end
15:43:01.770 6542-6569/com.example.kotlin I/yvan: time: 3010

7.3 挂起函数

我们先来看一段代码,其中delay方法是否能正常编译通过呢?

    fun delayTest(){
        delay(1000)
    }

以上代码会报错:Suspend function ‘delay’ should be called only from a coroutine or another suspend function
为什么呢?我们来看挂起函数的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)
        }
    }
}

可以看到,方法签名用 suspend 修饰,表示该函数是一个挂起函数。解决这个异常,只需要将我们定义的方法也用 suspend 修饰,使其变成一个挂起函数
使用 suspend 关键字修饰的函数成为挂起函数挂起函数只能在另一个挂起函数,或者协程中被调用。在挂起函数中可以调用普通函数(非挂起函数)。

7.4 协程和挂起的本质

本质上,协程是轻量级的线程,kotlin 协程的实现是借助线程,可以理解为对线程的一个封装框架。启动一个协程,使用 launch 或者 async 函数,启动的是函数中闭包代码块,好比启动一个线程,实现上是执行 run 方法中的代码,所以协程可以理解为是这个代码块。协程的核心点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。

suspend 翻译过来是,中断、暂停的意思。当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。这个挂起,是针对当前线程来说的,从当前线程挂起,就是这个协程从执行它的线程上脱离,并不是说协程停下来了,而是当前线程不再管这个协程要去做什么了。

当协程执行到挂起函数时,从当前线程脱离,然后继续执行,这个时候在哪个线程执行,由协程调度器所指定,挂起函数执行完之后,又会重新切回到它原先的线程来,这个就是协程的优势所在。

理解一下协程和线程的区别:

  1. 线程一旦开始执行就不会暂停,直到任务结束,这个过程是连续的,线程是抢占式的调度,不存在协作的问题
  2. 协程程序能够自己挂起和恢复,程序自己处理挂起恢复实现程序执行流程的协作式调度。

Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作,这个 resume 功能是协程的,如果不在协程里面调用,那它就没法恢复。所以挂起函数必须在协程或者另一个挂起函数里面被调用,总是直接或者间接地在协程里被调用

实现挂起的的目的是让程序脱离当前的线程,也就是要切线程,kotlin 协程提供了一个 withContext() 方法,来实现线程切换

    private suspend fun withContextTest() {
        withContext(Dispatchers.IO) {
            Log.i("yvan", "withContextTest")
        }
    }

withContext() 本身也是一个挂起函数,它接收一个 Dispatcher参数,依赖这个参数,协程被挂起再切到别的线程。所以想要自己写一个挂起函数,除了加上 suspend 关键字以外,还需要函数内部直接或者间接的调用 Kotlin 协程框架自带的挂起函数才行。比如前面调用的 delay 函数,框架内部实际上进行了切线程的操作。

suspend 并不能切换线程。切线程依赖的是挂起函数里面的实际代码,这个关键字,只是一个提醒作用。如果我创建一个 suspend 函数,内部不包含其它挂起函数,编译器同样会提示这个修饰符是多余的。

suspend 表明这个函数时挂起函数,限制了它只能在协程或者其它挂起函数里面调用

其它语言,比如 C#,使用的 async 关键字。

如果一个函数比较耗时,那么就可以把它定义成挂起函数耗时一般有两种情况: I/O 操作CPU 计算工作
另外还有延时操作也可以把它定义成挂起函数,代码本身执行不耗时,但是需要延时一段时间。

写法:
给函数加上 suspend 关键字后

  1. 如果是耗时操作:在 withContext 把函数的内容操作就可以了
  2. 如果是延时操作:调用 delay 函数即可。

延时操作:

suspend fun testA() {
    ...
    delay(1000)
    ...
}

耗时操作:

suspend fun testB() {
    withContext(Dispatchers.IO) {
        ...
    }
}

// 也可以写成:
suspend fun testB() = withContext(Dispatchers.IO) {
    ...
}

8 协程上下文和作用域

两个概念:

  1. CoroutineContext 协程的上下文
  2. CoroutineScope 协程的作用域

8.1 协程上下文 CoroutineContext

协程总是运行在一些以 CoroutineContext 类型为代表的上下文中。协程上下文是各种不同元素的集合。其中主元素是协程中的 Job 以及它的调度器。

协程上下文包含当前协程scope的信息, 比如的Job, ContinuationInterceptor, CoroutineNameCoroutineId。在CoroutineContext中,是用map来存这些信息的, map的键是这些类的伴生对象,值是这些类的一个实例,你可以这样子取得context的信息:

val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]

Job继承了CoroutineContext.ElementCoroutineContext.Element继承了 CoroutineContext。 他是协程上下文的一部分。 Job 一个重要的子类 ———— AbstractCoroutine,即协程。使用launch 或者async方法都会实例化出一个AbstractCoroutine 的协程对象。一个协程的协程上下文的Job值就是他本身。

val job = mScope.launch {
        printWithThreadInfo("job: ${this.coroutineContext[Job]}")
    }
    printWithThreadInfo("job2: $job")
    printWithThreadInfo("job3: ${job[Job]}")

输出:

thread id: 1, thread name: main —> job2: StandaloneCoroutine{Active}@1ee0005
thread id: 12, thread name: test_dispatcher —> job: StandaloneCoroutine{Active}@1ee0005
thread id: 1, thread name: main —> job3: StandaloneCoroutine{Active}@1ee0005
协程上下文包含一个 协程调度器 (CoroutineDispatcher)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

所有的协程构建器诸如 launchasync 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。

当调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)。

CoroutineContext最重要的两个信息是 DispatcherJob, 而 DispatcherJob 本身又实现了 CoroutineContext 的接口。是其子类。
这个设计就很有意思了。
有时我们需要在协程上下文中定义多个元素。我们可以使用 + 操作符来实现。 比如说,我们可以显式指定一个调度器来启动协程并且同时显式指定一个命名:

launch(Dispatchers.Default + CoroutineName("test")) {
    println("I'm working in thread ${Thread.currentThread().name}")
}

这得益于 CoroutineContext 重载了操作符 +

8.2 协程作用域 CoroutineScope

CoroutineScope 即协程运行的作用域,它的源码如下:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

可以看出CoroutineScope的代码很简单,主要作用是提供 CoroutineContext, 启动协程需要 CoroutineContext

作用域可以管理其域内的所有协程。一个CoroutineScope可以有许多的子scope。协程内部是通过 CoroutineScope.coroutineContext 自动继承自父协程的上下文。而 CoroutineContext 就是在作用域内为协程进行线程切换的快捷方式。

当使用 GlobalScope 来启动一个协程时,则新协程的作业没有父作业。 因此它与这个启动的作用域无关且独立运作。GlobalScope 包含的是 EmptyCoroutineContext

一个父协程总是等待所有的子协程执行结束。父协程并不显式的跟踪所有子协程的启动,并且不必使用 Job.join 在最后的时候等待它们。

取消父协程会取消所有的子协程。所以使用 Scope 来管理协程的生命周期。

默认情况下,协程内,某个子协程抛出一个非 CancellationException 异常,未被捕获,会传递到父协程,任何一个子协程异常退出,那么整体都将退出

8.3 创建 CoroutineScope

创建一个 CoroutineScope, 只需调用 public fun CoroutineScope(context: CoroutineContext) 方法,传入一个 CoroutineContext 对象。

在协程作用域内,启动一个子协程,默认自动继承父协程的上下文,但在启动时,我们可以指定传入上下文。

val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
val myScope = CoroutineScope(dispatcher)
myScope.launch {
    ...
}

8.4 SupervisorJob

启动一个协程,默认是实例化的是 Job 类型。该类型下,协程内,某个子协程抛出一个非 CancellationException 异常,未被捕获,会传递到父协程,任何一个子协程异常退出,那么整体都将退出。

为了解决上述问题,可以使用SupervisorJob替代JobSupervisorJobJob基本类似,区别在于SupervisorJob不会被子协程的异常所影响

private val svJob = SupervisorJob()
private val mDispatcher = newSingleThreadContext("test_dispatcher")
 
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    printWithThreadInfo("exceptionHandler: throwable: $throwable")
}
 
private val svScope = CoroutineScope(svJob + mDispatcher + exceptionHandler)
private val mScope = CoroutineScope(Job() + mDispatcher + exceptionHandler)
 
svScope.launch {
    ...
}
 
// 或者
supervisorScope { 
    launch { 
        ...
    }
}

8.5 如何在 Android 中使用协程

8.5.1. 自定义 coroutineScope

不要使用 GlobalScope 去启动协程,因为 GlobalScope 启动的协程生命周期与应用程序的生命周期一致,无法取消。官方建议在 Android 中自定义协程作用域。当然Kotlin 给我们提供了 MainScope,我们可以直接使用。

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

然后让 Activity 实现该作用域:

class BasicCorotineActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    ...
}

然后再通过 launch 或者 async 启动协程

private fun loadAndShow() {
    launch {
        val task = async(Dispatchers.IO) {
            // load 过程
            delay(3000)
            ...
            "hello, kotlin"
        }
        tvShow.setText(task.await())
    }
}

最后别忘了,在 Activity onDestory 时取消协程。

override fun onDestroy() {
    cancel()
    super.onDestroy()
}

8.5.2 ViewModelScope

如果你使用了 ViewModel + LiveData 实现 MVVM 架构,根本就不会在 Activity 上书写任何逻辑代码,更别说启动协程了。这个时候大部分工作就要交给 ViewModel 了。那么如何在 ViewModel 中定义协程作用域呢?直接把上面的 MainScope() 搬过来就可以了。

class ViewModelOne : ViewModel() {
 
    private val viewModelJob = SupervisorJob()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
 
    val mMessage: MutableLiveData<String> = MutableLiveData()
 
    fun getMessage(message: String) {
        uiScope.launch {
            val deferred = async(Dispatchers.IO) {
                delay(2000)
                "post $message"
            }
            mMessage.value = deferred.await()
        }
    }
 
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
}

这里的 uiScope 其实就等同于 MainScope。调用 getMessage() 方法和之前的 loadAndShow() 效果也是一样的,记得在 ViewModel 的 onCleared() 回调里取消协程。

你可以定义一个 BaseViewModel 来处理这些逻辑,避免重复书写模板代码。

然而,Kotlin 提供了 viewmodel-ktx 来了。引入下面的依赖:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha03"

然后直接使用协程作用域 viewModelScope 就可以了。viewModelScopeViewModel 的一个扩展属性,定义如下:

val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
    }

所以,直接使用 viewModelScope 就是最好的选择。

8.5.3 LifecycleScope

viewModelScope 配套的 还有 LifecycleScope, 引入依赖:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03"

lifecycle-runtime-ktx 给每个 LifeCycle 对象通过扩展属性定义了协程作用域 lifecycleScope 。可以通过 lifecycle.coroutineScope 或者 lifecycleOwner.lifecycleScope 进行访问。示例代码如下:

lifecycleOwner.lifecycleScope.launch {
    val deferred = async(Dispatchers.IO) { 
        getMessage("LifeCycle Ktx")
    }
    mMessage.value = deferred.await()
}

LifeCycle 回调 onDestroy() 时,协程作用域 lifecycleScope 会自动取消。

9 协程并发数据同步

9.1 线程中数据安全问题

在多线程同时操作修改一个数据时,可能会出现数据异常的情况,我们称之为线程数据不安全,给数据加上 volatile 关键修饰:

@Volatile
var data = 1

没有用 volatile 修饰 data 之前,改变了不具有可见性,一个线程将它的值改变后,另一个线程却 “不知道”,所以程序没有退出。

当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU缓存中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过CPU缓存这一步。

volatile 修饰的遍历具有如下特性:

  1. 保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
  2. 禁止指令重排序优化。
  3. 不会阻塞线程。

synchronized 只会保证该同步块中的变量的可见性,发生变化后立即同步到主存,JVM对于现代的机器做了最大程度的优化,也就是说,最大程度的保障了线程和主存之间的及时的同步,也就是相当于虚拟机尽可能的帮我们加了个volatile,但是,当CPU被一直占用的时候,同步就会出现不及时的情况。

9.2 协程中数据同步问题

看如下例子:

  	CoroutineScope(Dispatchers.IO).launch {
        concurrencyTest()
    }
        
    private var count = 0
    private suspend fun concurrencyTest() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    count++
                }
            }
        }
        launch {
            delay(3000)
            Log.i("yvan", "end count: $count thread:${Thread.currentThread().name}")
        }
    }

打印结果:

yvan: end count: 96137 thread:DefaultDispatcher-worker-5

并不是我们期待的 100000。很明显,协程并发过程中数据不同步造成。

9.2.1 volatile 无效?

很显然,有人肯定也想着,使用 volatile 修饰变量,就可以解决,真的是这样吗?其实不然。我们给 count 变量用 volatile 修饰也依然得不到期望的结果。

volatile 在并发中保证可见性,但是不保证原子性。 count++ 该运算,包含读、写操作,并非一次原子操作。这样并发情况下,自然得不到期望的结果。

9.2.2 使用线程安全的数据结构

一种解决办法是使用线程安全地数据结构。们可以使用具有 incrementAndGet 原子操作的 AtomicInteger 类:

  	CoroutineScope(Dispatchers.IO).launch {
        concurrencyTest()
    }
    private var count = AtomicInteger()
    private suspend fun concurrencyTest() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    count.incrementAndGet()
                }
            }
        }
        launch {
            delay(3000)
            Log.i("yvan", "end count: ${count.get()} thread:${Thread.currentThread().name}")
        }
    }

打印结果:

yvan: end count: 100000 thread:DefaultDispatcher-worker-7

9.2.3 同步操作

对数据的增加进行同步操作,可以同步计数自增的代码块:

    private val obj = Any()

    private var count = 0
    private suspend fun concurrencyTest() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    synchronized(obj) {  // 同步代码块
                        count++
                    }
                }
            }
        }
        launch {
            delay(3000)
            Log.i("yvan", "end count: $count thread:${Thread.currentThread().name}")
        }
    }

或者使用 ReentrantLock 操作。


   runBlocking<Unit> {
       val cos = measureTimeMillis {
            concurrencyTest()
        }
        Log.i(
            "yvan", "cos time: $cos"
        )
    }

 	private val mLock = ReentrantLock()
    private var count = 0
    private suspend fun concurrencyTest() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    mLock.lock()
                    try{
                        count++
                    } finally {
                        mLock.unlock()
                    }
                }
            }
        }
        launch {
            delay(3000)
            Log.i("yvan", "end count: $count thread:${Thread.currentThread().name}")
        }
    }
   

打印结果:

yvan: end count: 100000 thread:DefaultDispatcher-worker-53
yvan: cos time: 3275

加锁协程中的替代品叫做 Mutex, 它具有 lockunlock 方法,关键的区别在于, Mutex.lock() 是一个挂起函数,它不会阻塞当前线程。还有 withLock 扩展函数,可以方便的替代常用的 mutex.lock();try { …… } finally { mutex.unlock() } 模式:

   runBlocking<Unit> {
      val cos = measureTimeMillis {
            concurrencyTest()
        }
        Log.i(
            "yvan", "cos time: $cos"
        )
    }
        
    private val mutex = Mutex()

    private var count = 0
    private suspend fun concurrencyTest() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    mutex.withLock {
                        count++
                    }
                }
            }
        }
        launch {
            delay(3000)
            Log.i("yvan", "end count: $count thread:${Thread.currentThread().name}")
        }
    }

打印结果:

yvan: end count: 100000 thread:DefaultDispatcher-worker-45
yvan: cos time: 3040

9.2.4 限制线程

在同一个线程中进行计数自增,就不会存在数据同步问题。每次进行自增操作时,切换到单一线程。如同 Android,UI 刷新必须切换到主线程一般。

    runBlocking<Unit> {
        val cos = measureTimeMillis {
           singleThreadLimit()
        }
        Log.i(
            "yvan", "cos time: $cos"
        )
    }
    private val countContext = newSingleThreadContext("CountContext")

    private var count = 0
    suspend fun singleThreadLimit() = withContext(countContext) {
        repeat(100) {
            launch {
                repeat(1000) {
                    count++
                }
            }
        }
        launch {
            delay(3000)
            Log.i("yvan", "end count: $count thread:${Thread.currentThread().name}")
        }
    }

打印结果:

yvan: end count: 100000 thread:CountContext
yvan: cos time: 3014

9.2.5 使用 Actors

一个 actor 是由协程被限制并封装到该协程中的状态以及一个与其它协程通信的通道 组合而成的一个实体。一个简单的 actor 可以简单的写成一个函数, 但是一个拥有复杂状态的 actor 更适合由类来表示。

有一个 actor 协程构建器,它可以方便地将 actor 的邮箱通道组合到其作用域中(用来接收消息)、组合发送 channel 与结果集对象,这样对 actor 的单个引用就可以作为其句柄持有。

使用 actor 步骤:

  1. 是定义一个 actor 要处理的消息类,Kotlin 的密封类很适合这种场景。 我们使用 IncCounter 消息(用来递增计数器)GetCounter 消息(用来获取值)来定义 CounterMsg 密封类。 后者需要发送回复。CompletableDeferred 通信原语表示未来可知(可传达)的单个值, 这里被用于此目的。
// 计数器 Actor 的各种类型
sealed class CounterMsg
// 递增计数器的单向消息
object IncCounter : CounterMsg() 
// 携带回复的请求
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() 
  1. 接下来定义一个函数,使用 actor 协程构建器来启动一个 actor
// 这个函数启动一个新的计数器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
	// actor 状态
    var counter = 0 
    // 即将到来消息的迭代器
    for (msg in channel) { 
        when (msg) {
            is IncCounter -> counter++
            is GetCounter -> msg.response.complete(counter)
        }
    }
}

主要代码:

    suspend fun counterActorTest() = withContext(Dispatchers.IO) {
    	// 创建该 actor
        val counterActor = counterActor() 
        repeat(100) {
            launch {
                repeat(1000) {
                    counterActor.send(IncCounter)
                }
            }
        }
        launch {
            delay(3000)
            // 发送一条消息以用来从一个 actor 中获取计数值
            val response = CompletableDeferred<Int>()
            counterActor.send(GetCounter(response))
            Log.i("yvan", "Counter = ${response.await()}")
            // 关闭该actor
            counterActor.close() 
        }
    }

actor 本身执行时所处上下文(就正确性而言)无关紧要。一个 actor 是一个协程,而一个协程是按顺序执行的,因此将状态限制到特定协程可以解决共享可变状态的问题。实际上,actor 可以修改自己的私有状态, 但只能通过消息互相影响(避免任何锁定)。

actor 在高负载下比锁更有效,因为在这种情况下它总是有工作要做,而且根本不需要切换到不同的上下文。

实际上, CoroutineScope.actor()方法返回的是一个 SendChannel对象。Channel 也是 Kotlin 协程中的一部分。

10 协程总结

10.1 CoroutineContext

协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map集合,可以通过key来获取不同类型的数据。同时CoroutineContext的灵活性很强,如果其需要改变只需使用当前的CoroutineContext来创建一个新的CoroutineContext即可。
在这里插入图片描述
在这里插入图片描述

10.2 CoroutineScope

我们可以认为CoroutineScope是提供CoroutineContext的容器,保证CoroutineContext能在整个协程运行中传递下去,约束CoroutineContext的作用边界。
在这里插入图片描述

10.3 GlobalScope

  • GlobalScope(object关键词修饰,其实就是个单例)不受job任何边界限制。
  • GlobalScope用于启动顶级协程,在整个应用程序生命周期内运行且不会过早取消。
  • GlobalScope的另一种用法是在Dispatchers.Unconfined中运行的操作符,它与job无任何关联。
  • 应用程序代码通常应使用应用程序定义的CoroutineScope
  • 不建议GlobalScope在应用中使用。
lifecycleScope
lifecycleScope.launch(Dispatchers.IO) {

}

在这里插入图片描述

10.4 ViewModelScope

ViewModelScope是为 ViewModel应用程序中的每个定义的。如果清除,在此范围内启动的任何协程都会自动取消ViewModel。当您只有在活动时才需要完成工作时,协程非常有用ViewModel。例如,如果您正在计算布局的一些数据,则应将工作范围限制在 ,ViewModel以便在 ViewModel清除 时,工作会自动取消以避免消耗资源。
ViewModel中使用的协程。 它是ViewModel的扩展属性。自动取消,不会造成内存泄漏,如果是CoroutineScope,就需要在onCleared()`方法中手动取消了,否则可能会造成内存泄漏。

10.5 CoroutineStart-协程启动模式

在这里插入图片描述
在这里插入图片描述

suspend fun main() {    
    println(1)    
    val job = GlobalScope.launch {       
         println(2)    
    }
    println(3)
    //等待协程执行完毕
    job.join()
    println(4)
}

// print 1 3 2 4
suspend fun main() {    
    println(1)    
    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
         println(2)    
    }
    println(3)
    //等待协程执行完毕
    job.join()
    println(4)
}

// 1 3 4 2 

10.6 Dispatchers

协程上下文包含一个 协程调度器 (参见 CoroutineDispatcher)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
所有的协程构建器诸如 launchasync 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。

ContinuationInterceptor
在这里插入图片描述
初看起来这种写法有点奇怪,但习惯了以后还是不得不承认这个是很优雅的设计(相当于一个协变类型的 map)。

10.7 CPS — Continuation Passing Style

CPS其实就是将直接返回值的函数,变换为通过回调传递结果的函数在这里插入图片描述
很简单吧?这就是CPS风格,函数的结果通过回调来传递, 协程里通过在CPSContinuation回调里结合状态机流转,来实现协程挂起-恢复的功能.

Kotlin 中被 suspend 修饰符修饰的函数在编译期间会被编译器做特殊处理。而这个特殊处理的第一道工序就是:CPS(续体传递风格)变换,它会改变挂起函数的函数签名。
我们直接展示一个例子:

挂起函数 await 的函数签名如下所示:

suspend fun <T> CompletableFuture<T>.await(): T

在编译期发生 CPS 变换之后:

fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

编译器对挂起函数的第一个改变就是对函数签名的改变,这种改变被称为 CPS(续体传递风格)变换。

我们看到发生 CPS 变换后的函数多了一个 Continuation<T> 类型的参数,Continuation 这个单词翻译成中文就是续体,它的声明如下:

interface Continuation<in T> {
   val context: CoroutineContext
   fun resumeWith(result: Result<T>)
}

续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。在 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行 await 函数后面的代码。

11 协程面试题

1、什么是 Kotlin 协程?

Kotlin 协程是一种轻量级的并发框架,用于简化异步编程。它允许开发者使用顺序的方式来编写异步的、非阻塞的代码,提供了一种能够挂起和恢复执行的机制。

2、Kotlin 协程与线程的区别是什么?

Kotlin 协程基于线程的,但是它们更轻量级更高效。线程是操作系统调度的最小执行单位,而协程是在运行时进行调度的,可以允许更多的协程在较少的线程上执行。

3、如何创建一个协程?

可以使用 launch 函数或async函数来创建一个协程。例如,launch { ... } 可以创建一个顶层协程,它将在协程作用域中运行。

4、协程的取消机制是什么?

协程的取消可以通过调用 cancel 方法或者取消相关的协程作用域来实现。协程会在取消后立即停止执行,并调用相应的取消回调。

5、如何处理协程中的异常?

可以使用 try/catch 块来捕获协程中的异常。可以使用 CoroutineExceptionHandler 来设置一个统一的异常处理程序。

6、什么是挂起函数?

挂起函数是指在执行期间可能会暂停执行的函数。它们通过使用 suspend 修饰符来定义,可以被其他协程挂起和恢复执行。

7、协程的调度器是什么?

协程的调度器是负责决定协程在哪个线程上执行的组件。Kotlin 协程的调度器可以通过 launchasync 等函数的参数来指定,也可以使用 withContext 函数在协程内部切换调度器。

8、协程的上下文是什么?

协程的上下文是一组键值对,包含了协程的调度器异常处理器等信息。可以使用 CoroutineScope 或者 coroutineScope函数来创建具有特定上下文的协程作用域

9、协程的并发与并行有何区别?

协程的并发是指在同一个线程上进行交替执行的能力,通过使用协程挂起和恢复执行的机制来实现。而并行是指在不同的线程上同时执行多个任务。

10、什么是协程的父子关系?

协程可以嵌套在其他协程中,形成父子关系。父协程在执行时会等待其所有子协程执行完毕,这样可以实现更好的结构化并发。

11、协程的优势有哪些?

协程具有以下优势:

  1. 简化异步编程,用顺序代码编写异步逻辑
  2. 更轻量级,可以在较少的线程上运行大量的协程
  3. 提供异常处理机制,使得错误处理更加灵活
  4. 支持结构化并发,提高代码的可读性和可维护性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/146650.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

相约黄浦江畔,汇聚AI与边缘计算的力量

很高兴告诉您&#xff1a;全球边缘计算大会上海站即将盛大启幕&#xff01; 第八届全球边缘计算大会将于12月16日&#xff08;周六&#xff09;在上海黄浦江旁边的三至喜来登酒店召开&#xff0c;距离这场边缘计算年度盛会开幕仅剩最后35天啦&#xff01; 参会收益 开阔视野&am…

咱就是问坐发动机上跑得快吗?意塔杰特Dragster559 Twin

这家伙可以说是这次米兰车展踏板车里面最怪的了&#xff0c;不过性能方面有点出众&#xff0c;外观也是极为独特的。 车架采用的是裸露的编制车架&#xff0c;这也符合这个品牌的风格&#xff0c;这也是他们家的一个卖点了&#xff0c;不过这个车好像还是工程样车的状态&#x…

并发编程由浅及深(一)

并发编程重要吗&#xff1f;当然重要&#xff0c;因为并发在我们的项目中真实存在&#xff0c;如果你不能充分了解它那么很可能造成严重的生产事故。最近隔壁项目组出了一个问题&#xff0c;每次请求接口之后都发现线程固定增加了5个&#xff0c;而且线程数一直增加没有减少&am…

景联文科技:驾驭数据浪潮,赋能AI产业——全球领先的数据标注解决方案供应商

根据IDC相关数据统计&#xff0c;全球数据量正在经历爆炸式增长&#xff0c;预计将从2016年的16.1ZB猛增至2025年的163ZB&#xff0c;其中大部分是非结构化数据&#xff0c;被直接利用&#xff0c;必须通过数据标注转化为AI可识别的格式&#xff0c;才能最大限度地发挥其应用价…

飞书开发学习笔记(六)-网页应用免登

飞书开发学习笔记(六)-网页应用免登 一.上一例的问题修正 在上一例中&#xff0c;飞书登录查看网页的界面显示是有误的&#xff0c;看了代码&#xff0c;理论上登录成功之后&#xff0c;应该显示用户名等信息。 最后的res.nickName是用户名&#xff0c;res.i18nName.en_us是英…

leetcode二分查找算法题

目录 1.二分查找2.在排序数组中查找元素的第一个和最后一个位置3.x的平方根4.搜索插入位置5.山脉数组的峰顶索引6. 寻找峰值7.寻找旋转排序数组中的最小值8.8.0~n-1中缺失的数字 1.二分查找 二分查找 class Solution { public:int search(vector<int>& nums, int …

Ubuntu 17.10 “Artful Aardvark” 发布首个 Beta

Ubuntu 17.10 “Artful Aardvark” 首个 Beta 版已发布。 按照 Ubuntu 17.10 的发布日程 &#xff0c;Ubuntu 17.10 首个 beta 版按时发布了。不过参与本次测试版的没有 Ubuntu 官方风味版本&#xff08;要尝试的话可以考虑每日构建 ISO&#xff09;&#xff0c;包括了 Kubunt…

uniapp插件开发

安装android studio&#xff1a;安装目录下bin下的此文件&#xff0c;是用来修改分配给android studio的占用内存。 Android 11足够用。 创建新项目&#xff1a; 目录结构介绍&#xff1a; UI组件介绍&#xff1a;在设计程序界面时可以使用可视化拖拽的方式&#xff0c;没有必要…

RT-Thread STM32F407 五步完成OLED移植

这里使用RT-Thread Studio提供的IIC API驱动函数进行移植 第一步&#xff0c;进入RT-Thread Settings配置IIC驱动 第二步&#xff0c;进入board.h&#xff0c;定义IIC宏 第三步&#xff0c;进入STM32CubeMX&#xff0c;配置时钟及IIC 第四步&#xff0c;添加oled.c及oled…

Mozilla 面向基于 Debian 的 Linux 发行版

导读Mozilla 公司今天发布新闻稿&#xff0c;表示面向 Debian、Ubuntu 和 Linux Mint 等基于 Debian 的发行版&#xff0c;推出了.deb 格式的 Firefox Nightly 浏览器安装包&#xff0c;便于用户在上述发行版中更轻松地安装。 本次更新的亮点之一在于采用 APT 存储库&#xff0…

毫米波雷达模块的目标检测与跟踪

毫米波雷达技术在目标检测与跟踪方面具有独特的优势&#xff0c;其高精度、不受光照影响等特点使其在汽车、军事、工业等领域广泛应用。本文深入探讨毫米波雷达模块在目标检测与跟踪方面的研究现状、关键技术以及未来发展方向。 随着科技的不断进步&#xff0c;毫米波雷达技术在…

Zabbix 5.0部署(centos7+server+MySQL+Apache)

环境 系统IPZABBIX版本主机名centos7192.168.231.2195.0zabbix-server 安装zabbix 我选择版本是zabbix-5.0 zabbix的官网是Zabbix :: The Enterprise-Class Open Source Network Monitoring Solution 安装Zabbix软件源 rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/…

【开发工具】gitee还不用会?我直接拿捏 >_>

&#x1f308;键盘敲烂&#xff0c;年薪30万&#x1f308; 目录 git的一些前置操作 如何获取本地仓库 本地仓库的操作 远程仓库操作 合并两个仓库&#xff08;通用方法&#xff09; 从远程仓库拉取文件报错 fatal:refusing to merge unrelated histories 分支操作 注意&…

人工智能基础_机器学习033_多项式回归升维_多项式回归代码实现_非线性数据预测_升维后的数据对非线性数据预测---人工智能工作笔记0073

然后我们来实际的操作一下看看,多项式升维的作用,其实就是为了,来对,非线性的数据进行拟合. 我们直接看代码 import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression X=np.linspace(-1,11,num=100) 从-1到11中获取100个数…

MVC使用的设计模式

MVC使用的设计模式 一、背景 MVC模式是"Model-View-Controller"的缩写&#xff0c;中文翻译为"模式-视图-控制器"。MVC应用程序总是由这三个部分组成。Event(事件)导致Controller改变Model或View&#xff0c;或者同时改变两者。只要Controller改变了Model…

No210.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

JVM虚拟机:垃圾回收之三色标记

本文重点 在前面的课程中我们已经学习了垃圾回收器CMS和G1,其中CMS和G1中的mixedGC都存在四个过程,这四个过程中有一个过程叫做并发标记,也就是说程序一边运行,一边标记垃圾。这个过程最困难的是:如果在标记垃圾的时候,如果对象的引用关系发生了改变,此时应该如何处理?…

Spark通过三种方式创建DataFrame

通过toDF方法创建DataFrame 通过toDF的方法创建 集合rdd中元素类型是样例类的时候&#xff0c;转成DataFrame之后列名默认是属性名集合rdd中元素类型是元组的时候&#xff0c;转成DataFrame之后列名默认就是_N集合rdd中元素类型是元组/样例类的时候&#xff0c;转成DataFrame…

【Python】一文带你掌握数据容器之集合,字典

目录&#xff1a; 一、集合 思考&#xff1a;我们目前接触到了列表、元组、字符串三个数据容器了。基本满足大多数的使用场景为何又需要学习新的集合类型呢? 通过特性来分析: &#xff08;1&#xff09;列表可修改、支持重复元素且有序 &#xff08;2&#xff09;元组、字符…

数据结构第四课 -----线性表之栈

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…