Kotlin
中的协程提供了一种全新处理并发的方式,您可以在 Android
平台上使用它来简化异步执行的代码。协程是从 Kotlin 1.3 版本开始引入,但这一概念在编程世界诞生的黎明之际就有了,最早使用协程的编程语言可以追溯到 1967 年的 Simula
语言。
在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 Javascript
、C#
、Python
、Ruby
以及 Go
等。Kotlin
的协程
是基于来自其他语言的既定概念。
在 Android
平台上,协程
主要用来解决两个问题:
处理耗时任务 (Long running tasks),这种任务常常会阻塞住主线程
;保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数
。特点一句话总结:协程能更加安全实现异步代码同步化,实质是对线程切换的封装
下面我们来看看创建协程的三种方式:
fun runBlockingTest(){
runBlocking {
KyLog.i(
"yvan","runBlocking"
)
}
}
fun globalScopeTest(){
GlobalScope.launch {
Log.i(
"yvan","GlobalScope launch"
)
}
}
fun coroutineScopeTest(){
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
KyLog.i(
"yvan","CoroutineScope launch"
)
}
}
runBlocking
通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的,不推荐。GlobalScope
和使用方法一runBlocking
的区别在于不会阻塞线程。但在 Android
开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。CoroutineContext
是比较推荐的使用方法,我们可以通过 context
参数去管理和控制协程的生命周期(这里的 context
和 Android
里的不是一个东西,是一个更通用的概念,会有一个 Android
平台的封装来配合使用)。与线程类比,Java
线程其实没有提供任何机制来安全地终止线程。
Thread
类提供了一个方法 interrupt()
方法,用于中断线程的执行。调用interrupt()
方法并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息,然后由线程在下一个合适的时机中断自己。协程Job 接口有一个 cancel()
方法,用于取消它,调用它会触发以下效果:
job
(下面例子中的 delay)job
有几个子 job
,它们也会被取消(但是它的父 job
不受影响)job
被取消,它就不能被用作任何新 job
的父 job
。它首先处于 “Cancelling
” 状态,然后处于 “Cancelled
” 状态取消之后,我们通常会调用 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
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
这种做法不太好,所以请不要在任何现实项目中这么做。我们只是试图模拟一种情况,在这种情况下,我们广泛的使用我们的协程,但没有挂起它们。在实践中,如果我们有一些更复杂的计算,比如神经网络学习(是的,为了简化处理并行化,我们也会使用协程),或者当我们需要做一些阻塞调用(例如,读取文件)时,就会发生这种情况。
使用 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 中,当用户离开一个视图时,我们需要取消此视图启动的多个协程。
当一个 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
块来关闭文件
或数据库连接
等。
由于我们可以捕获 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
内不会再执行。
有时,当协程已经取消时,我们确实需要使用挂起函数。在这种情况下,首选的方法是使用 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
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
是一个异常:
null
CancellationException
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次,每次sleep
200ms执行,所以总的时间为80000ms。
我们可以使用 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
我们也可以使用 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()
需要在 CoroutinScope
、CoroutineContext
、Job
作用域内调用。它所做的事情只是在 job
不再活跃时抛出异常。它更轻量,所以通常它应该是首选。函数 yield
是一个常规的顶层挂起函数。它不需要任何作用域,因此可以在任意常规挂起函数中使用。由于它执行挂起和恢复操作,因此可能会产生其它影响,例如,如果我们使用带有线程池的分发器,则会导致线程更改。 yield
通常只用于挂起 CPU
密集型或阻塞线程的函数。
suspendCancellableCoroutine
它的行为类似于 suspendCoroutine
,但是它的 continuation
被包装到了提供了额外方法的 CancellableContinuation
中。最重要的一个方法是 invokeOnCancellation
,我们使用它来定义取消协程时应该发生什么。我们通常使用它来取消库中的进程或者释放一些资源。
suspend fun someTask() = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation {
// do cleanup
}
// rest of the implementation
}
CancellableContinuation
也允许我们检查 job
的状态(通过使用 isActive
,isCompleted
、 isCancelled
属性),并使用可选的取消原因(异常)取消这个 continuation
。
取消是一个强大的功能。它通常很容易使用,但有时会很棘手。所以,了解它的工作原理很重要。
正确使用取消操作意味着更少的资源浪费和更少的内存泄漏,这对我们的应用程序的性能很重要。
launch
函数创建async
函数创建。使用 async
方法启动 Deferred
(也是一种 job
), 可以调用它的 await()
方法获取执行的结果。
private suspend fun asyncTest(): Unit = coroutineScope {
val asyncDeferred = async {
// do some
}
// 等待结果返回
val result = asyncDeferred.await()
}
deferred
也是可以取消的,对于已经取消的 deferred
调用 await()
方法,会抛出JobCancellationException
异常。deferred.await
之后调用 deferred.cancel()
,那么什么都不会发生,因为任务已经结束了。上面协程取消中已经提到过,挂起函数包裹在 try/catch
代码块中,这样就可以在 finally
代码块中进行资源清理等操作了,具体请看3.2
绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 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
,两个函数正常执行完后都有返回值,但两者的区别在于:
withTimeout
超时则无返回值,直接抛出一个超时异常 TimeoutCancellationException
withTimeoutOrNull
函数会在超时也会有返回一个 null
值考虑一个场景: 开启多个任务,并发执行,所有任务执行完之后,返回结果,再汇总结果继续往下执行。
针对这种场景,解决方案有很多,比如 Java
的 FeatureTask
, concurrent
包里面的 CountDownLatch
、Semaphore
、 Rxjava
提供的 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
方法后,会阻塞,等待结果的返回,同样能达到效果。
async
可以通过将 start
参数设置为 CoroutineStart.LAZY
变成惰性的。在这个模式下,调用 await
获取协程执行结果的时候,或者调用 Job
的 start
方法时,协程才会启动。
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
我们先来看一段代码,其中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
关键字修饰的函数成为挂起函数,挂起函数只能在另一个挂起函数,或者协程中被调用
。在挂起函数中可以调用普通函数(非挂起函数)。
本质上,协程是轻量级的线程
,kotlin 协程的实现是借助线程,可以理解为对线程的一个封装框架。启动一个协程,使用 launch
或者 async
函数,启动的是函数中闭包代码块,好比启动一个线程,实现上是执行 run 方法中的代码,所以协程可以理解为是这个代码块。协程的核心点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。
suspend
翻译过来是,中断、暂停的意思。当线程执行到协程的 suspend
函数的时候,暂时不继续执行协程代码了。这个挂起,是针对当前线程来说的,从当前线程挂起,就是这个协程从执行它的线程上脱离,并不是说协程停下来了,而是当前线程不再管这个协程要去做什么了。
当协程执行到挂起函数时,从当前线程脱离,然后继续执行,这个时候在哪个线程执行,由协程调度器所指定,挂起函数执行完之后,又会重新切回到它原先的线程来,这个就是协程的优势所在。
理解一下协程和线程的区别:
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
关键字后
耗时操作
:在 withContext
把函数的内容操作就可以了延时操作
:调用 delay
函数即可。延时操作:
suspend fun testA() {
...
delay(1000)
...
}
耗时操作:
suspend fun testB() {
withContext(Dispatchers.IO) {
...
}
}
// 也可以写成:
suspend fun testB() = withContext(Dispatchers.IO) {
...
}
两个概念:
CoroutineContext
协程的上下文CoroutineScope
协程的作用域协程总是运行在一些以 CoroutineContext
类型为代表的上下文中。协程上下文是各种不同元素的集合。其中主元素是协程中的 Job
以及它的调度器。
协程上下文包含当前协程scope的信息, 比如的Job
, ContinuationInterceptor
, CoroutineName
和CoroutineId
。在CoroutineContext
中,是用map
来存这些信息的, map的键是这些类的伴生对象,值是这些类的一个实例,你可以这样子取得context
的信息:
val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]
Job
继承了CoroutineContext.Element
,CoroutineContext.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)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
所有的协程构建器诸如 launch
和 async
接收一个可选的 CoroutineContext
参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。
当调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope
中承袭了上下文(以及调度器)。
CoroutineContext
最重要的两个信息是 Dispatcher
和 Job
, 而 Dispatcher
和 Job
本身又实现了 CoroutineContext
的接口。是其子类。
这个设计就很有意思了。
有时我们需要在协程上下文中定义多个元素。我们可以使用 + 操作符来实现。 比如说,我们可以显式指定一个调度器来启动协程并且同时显式指定一个命名:
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
这得益于 CoroutineContext
重载了操作符 +
。
CoroutineScope
即协程运行的作用域,它的源码如下:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
可以看出CoroutineScope
的代码很简单,主要作用是提供 CoroutineContext
, 启动协程需要 CoroutineContext
。
作用域可以管理其域内的所有协程。一个CoroutineScope
可以有许多的子scope
。协程内部是通过 CoroutineScope.coroutineContext
自动继承自父协程的上下文。而 CoroutineContext
就是在作用域内为协程进行线程切换的快捷方式。
当使用
GlobalScope
来启动一个协程时,则新协程的作业没有父作业。 因此它与这个启动的作用域无关且独立运作。GlobalScope 包含的是EmptyCoroutineContext
。
一个父协程总是等待所有的子协程执行结束。父协程并不显式的跟踪所有子协程的启动,并且不必使用 Job.join
在最后的时候等待它们。
取消父协程会取消所有的子协程。所以使用 Scope
来管理协程的生命周期。
默认情况下,协程内,某个子协程抛出一个非 CancellationException
异常,未被捕获,会传递到父协程,任何一个子协程异常退出,那么整体都将退出
创建一个 CoroutineScope
, 只需调用 public fun CoroutineScope(context: CoroutineContext)
方法,传入一个 CoroutineContext
对象。
在协程作用域内,启动一个子协程,默认自动继承父协程的上下文,但在启动时,我们可以指定传入上下文。
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
val myScope = CoroutineScope(dispatcher)
myScope.launch {
...
}
启动一个协程,默认是实例化的是 Job 类型。该类型下,协程内,某个子协程抛出一个非 CancellationException
异常,未被捕获,会传递到父协程,任何一个子协程异常退出,那么整体都将退出。
为了解决上述问题,可以使用SupervisorJob
替代Job
,SupervisorJob
与Job
基本类似,区别在于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 {
...
}
}
不要使用 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()
}
如果你使用了 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
就可以了。viewModelScope
是 ViewModel
的一个扩展属性,定义如下:
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
就是最好的选择。
与 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
会自动取消。
在多线程同时操作修改一个数据时,可能会出现数据异常的情况,我们称之为线程数据不安全,给数据加上 volatile 关键修饰:
@Volatile
var data = 1
没有用 volatile
修饰 data
之前,改变了不具有可见性,一个线程将它的值改变后,另一个线程却 “不知道”,所以程序没有退出。
当把变量声明为 volatile
类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
volatile
变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile
类型的变量时总会返回最新写入的值。
在访问volatile
变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile
变量是一种比sychronized
关键字更轻量级的同步机制。
当对非 volatile
变量进行读写的时候,每个线程先从内存
拷贝变量到CPU缓存
中。如果计算机有多个CPU
,每个线程可能在不同的CPU
上被处理,这意味着每个线程可以拷贝到不同的CPU缓存
中。
而声明变量是 volatile
的,JVM 保证了每次读变量都从内存
中读,跳过CPU缓存
这一步。
volatile
修饰的遍历具有如下特性:
可见性
,当一个线程修改了这个变量的值,volatile
保证了新值能立即同步到主内存
,以及每次使用前立即从主内存
刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。指令重排序
优化。synchronized
只会保证该同步块中的变量的可见性
,发生变化后立即同步到主存,JVM
对于现代的机器做了最大程度的优化,也就是说,最大程度的保障了线程和主存之间的及时的同步,也就是相当于虚拟机尽可能的帮我们加了个volatile
,但是,当CPU
被一直占用的时候,同步就会出现不及时的情况。
看如下例子:
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。很明显,协程并发过程中数据不同步造成。
很显然,有人肯定也想着,使用 volatile
修饰变量,就可以解决,真的是这样吗?其实不然。我们给 count
变量用 volatile
修饰也依然得不到期望的结果。
volatile 在并发中保证可见性,但是不保证原子性。 count++ 该运算,包含读、写操作,并非一次原子操作。这样并发情况下,自然得不到期望的结果。
一种解决办法是使用线程安全地数据结构。们可以使用具有 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
对数据的增加进行同步操作,可以同步计数自增的代码块:
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
, 它具有 lock
和 unlock
方法,关键的区别在于, 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
在同一个线程中进行计数自增,就不会存在数据同步问题。每次进行自增操作时,切换到单一线程。如同 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
一个 actor
是由协程
、 被限制并封装到该协程中的状态
以及一个与其它协程通信的通道
组合而成的一个实体。一个简单的 actor 可以简单的写成一个函数, 但是一个拥有复杂状态的 actor
更适合由类来表示。
有一个 actor
协程构建器,它可以方便地将 actor
的邮箱通道组合到其作用域中(用来接收消息)、组合发送 channel
与结果集对象,这样对 actor
的单个引用就可以作为其句柄持有。
使用 actor
步骤:
actor
要处理的消息类,Kotlin 的密封类
很适合这种场景。 我们使用 IncCounter 消息(用来递增计数器)
和 GetCounter 消息(用来获取值
)来定义 CounterMsg 密封类
。 后者需要发送回复。CompletableDeferred
通信原语表示未来可知(可传达)的单个值, 这里被用于此目的。// 计数器 Actor 的各种类型
sealed class CounterMsg
// 递增计数器的单向消息
object IncCounter : CounterMsg()
// 携带回复的请求
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg()
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 协程
中的一部分。
CoroutineContext
协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map集合,可以通过key来获取不同类型的数据。同时CoroutineContext
的灵活性很强,如果其需要改变只需使用当前的CoroutineContext
来创建一个新的CoroutineContext
即可。
CoroutineScope
我们可以认为CoroutineScope
是提供CoroutineContext
的容器,保证CoroutineContext
能在整个协程运行中传递下去,约束CoroutineContext
的作用边界。
GlobalScope
GlobalScope
(object
关键词修饰,其实就是个单例)不受job
任何边界限制。GlobalScope
用于启动顶级协程,在整个应用程序生命周期内运行且不会过早取消。GlobalScope
的另一种用法是在Dispatchers.Unconfined
中运行的操作符,它与job
无任何关联。CoroutineScope
。GlobalScope
在应用中使用。lifecycleScope
lifecycleScope.launch(Dispatchers.IO) {
}
ViewModelScope
是为 ViewModel
应用程序中的每个定义的。如果清除,在此范围内启动的任何协程都会自动取消ViewModel
。当您只有在活动时才需要完成工作时,协程非常有用ViewModel
。例如,如果您正在计算布局的一些数据,则应将工作范围限制在 ,ViewModel
以便在 ViewModel
清除 时,工作会自动取消以避免消耗资源。
ViewModel中使用的协程。 它是
ViewModel的扩展属性。自动取消,不会造成内存泄漏,如果是
CoroutineScope,就需要在
onCleared()`方法中手动取消了,否则可能会造成内存泄漏。
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
协程上下文包含一个 协程调度器 (参见 CoroutineDispatcher
)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
所有的协程构建器诸如 launch
和 async
接收一个可选的 CoroutineContext
参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。
ContinuationInterceptor
初看起来这种写法有点奇怪,但习惯了以后还是不得不承认这个是很优雅的设计(相当于一个协变类型的 map)。
CPS其实就是将直接返回值的函数,变换为通过回调传递结果的函数
很简单吧?这就是CPS风格
,函数的结果通过回调来传递, 协程里通过在CPS
的Continuation
回调里结合状态机流转,来实现协程挂起-恢复的功能.
Kotlin 中被 suspend
修饰符修饰的函数在编译期间会被编译器做特殊处理。而这个特殊处理的第一道工序就是:CPS(续体传递风格)变换
,它会改变挂起函数的函数签名。
我们直接展示一个例子:
挂起函数 await
的函数签名如下所示:
suspend fun <T> CompletableFuture<T>.await(): T
在编译期发生 CPS 变换
之后:
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
编译器对挂起函数的第一个改变就是对函数签名的改变,这种改变被称为 CPS(续体传递风格)
变换。
我们看到发生 CPS 变换
后的函数多了一个 Continuation
类型的参数,Continuation
这个单词翻译成中文就是续体
,它的声明如下:
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。在 await
函数的挂起结束以后,它会调用 continuation
参数的 resumeWith
函数,来恢复执行 await
函数后面的代码。
Kotlin 协程
是一种轻量级的并发框架
,用于简化异步编程
。它允许开发者使用顺序的方式来编写异步的、非阻塞
的代码,提供了一种能够挂起和恢复执行
的机制。
Kotlin 协程
是基于线程
的,但是它们更轻量级
、更高效
。线程是操作系统调度的最小执行单位,而协程是在运行时进行调度的,可以允许更多的协程在较少的线程上执行。
可以使用 launch
函数或async
函数来创建一个协程。例如,launch { ... }
可以创建一个顶层协程,它将在协程作用域中运行。
协程的取消可以通过调用 cancel
方法或者取消相关的协程作用域
来实现。协程会在取消后立即停止执行,并调用相应的取消回调。
可以使用 try/catch
块来捕获协程中的异常。可以使用 CoroutineExceptionHandler
来设置一个统一的异常处理程序。
挂起函数
是指在执行期间可能会暂停执行
的函数。它们通过使用 suspend
修饰符来定义,可以被其他协程挂起和恢复执行。
协程的调度器是负责决定协程在哪个线程上执行的组件。Kotlin 协程的调度器可以通过 launch
、async
等函数的参数来指定,也可以使用 withContext
函数在协程内部切换调度器。
协程的上下文是一组键值对
,包含了协程的调度器
、异常处理器
等信息。可以使用 CoroutineScope
或者 coroutineScope函数
来创建具有特定上下文的协程作用域
。
协程的并发是指在同一个线程上进行交替执行的能力,通过使用协程挂起和恢复执行的机制来实现。而并行是指在不同的线程上同时执行多个任务。
协程可以嵌套在其他协程中,形成父子关系。父协程在执行时会等待其所有子协程执行完毕,这样可以实现更好的结构化并发。
协程具有以下优势: