- 原文地址:Advanced Kotlin Coroutines tips and tricks
- 原文做者:Alex Saveau
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:nanjingboy
- 校对者:zx-Zhu
协程从 1.3 开始成为稳定版!前端
开始 Kotlin 协程很是简单:只需将一些耗时操做放在 launch
中便可,你作到了,对不?固然,这是针对简单的状况。但很快,并发与并行的复杂性会慢慢堆积起来。android
当你深刻研究协程时,如下是一些你须要知道的事情。ios
没有办法绕过它:在某些时候,你不得不用原生 Java 流。这里的问题(不少状况下 😉)是使用流将会堵塞当前线程。这在协程中是一个坏消息。如今,若是你想要取消一个协程,在可以继续执行以前,你不得不等待读写操做完成。git
做为一个简单可重复的例子,让咱们打开 ServerSocket
而且等待 1 秒的超时链接:github
runBlocking(Dispatchers.IO) {
withTimeout(1000) {
val socket = ServerSocket(42)
// 咱们将卡在这里直到有人接收该链接。难道你不想知道为何吗?😜
socket.accept()
}
}
复制代码
应该能够运行,对吗?不。后端
如今你的感觉有点像:😖。 那么咱们如何解决呢?bash
当 Closeable
APIs 构建良好时,它们支持从任何线程关闭流并适当地失败。并发
注意:一般状况下,JDK 中的 APIs 遵循了这些最佳实践,但需注意第三方
Closeable
APIs 可能并无遵循。 你被提醒过了。app
幸好 suspendCancellableCoroutine
函数,当一个协程被取消时咱们能够关闭任何流:socket
public suspend inline fun <T : Closeable?, R> T.useCancellably(
crossinline block: (T) -> R
): R = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation { this?.close() }
cont.resume(use(block))
}
复制代码
确保这适用于你正在使用的 API !
如今阻塞的 accept
调用被 useCancellably
包裹,该协程会在超时触发的时候失败。
runBlocking(Dispatchers.IO) {
withTimeout(1000) {
val socket = ServerSocket(42)
// 抛出 `SocketException: socket closed` 异常。好极了!
socket.useCancellably { it.accept() }
}
}
复制代码
成功!
若是你不支持取消怎么办?如下是你须要注意的事项:
onDestroy
中清理资源,这尤为重要。解决方法: 将协同程序移动到 ViewModel
或其余上下文无关的类中并订阅它的处理结果。Dispatchers.IO
来处理阻塞操做,由于这可让 Kotlin 留出一些线程来进行无限等待。suspendCancellableCoroutine
替换 suspendCoroutine
。launch
vs. async
因为上面关于这两个特性的回答已通过时,我想我会再次分析它们的差别。
launch
异常冒泡当一个协程崩溃时,它的父节点将被取消,从而取消全部父节点的子节点。一旦整个树节点中的协程完成取消操做,异常将会发送到当前上线文的异常处理程序。在 Android 中,这意味着 你的 程序将会 崩溃,而无论你使用什么来进行调度。
async
持有本身的异常这意味着 await()
显式处理全部异常,安装 CoroutineExceptionHandler
将无任何效果。
launch
“blocks” 父做用域虽然该函数会当即返回,但其父做用域将 不会 结束,直到使用 launch
构建的全部协程以某种方式完成。所以若是你只是想等待全部协程完成,在父做用域末尾调用全部子做业的 join()
就没有必要了。
与你指望的可能不一样,即便未调用 await()
,外部做用域仍将等待async
协程完成。
async
返回值这一部分至关简单:若是你须要协程的返回值,async
是惟一的选择。若是你不须要返回值,使用 launch
来建立反作用。而且在继续执行以前须要完成这些反作用才须要使用 join()
。
join()
vs. await()
join()
在 await()
时 不会 从新抛出异常。但若是发生错误,join()
会取消你的协程,这意味着在 join()
挂起后调用任何代码都不会起做用。
如今你了解了你所使用不一样构造器异常处理机制的差别,你会陷入两难境地:你想记录异常而不崩溃(因此咱们不能使用 launch
),可是你不想手动调用 try
/catch
(因此咱们不能使用 async
)。因此这让咱们无所适从?谢天谢地。
记录异常是 CoroutineExceptionHandler
派上用场的地方。但首先,让咱们花点时间了解在协程中抛出异常时究竟发生了什么:
Continuation
恢复。CancellationException
,那么将经过当前的 CoroutineContext
请求第一个 CoroutineExceptionHandler
。ServiceLoader
用于定位全局处理程序。考虑到这一点,咱们有如下几个选择:
launch
的任何协程崩溃时都会调用它(hacky)。GlobalScope
,或将处理程序添加到你使用的每一个做用域,但这很烦人并使日志记录由默认变成了可选。最后一个方案是所推荐的,由于它具备灵活性而且须要最少的代码和技巧。
对于应用程序范围内的做业,你将使用带有日志记录处理程序的 AppScope
。对于其余业务,你能够在日志记录崩溃的适当位置添加处理程序。
val LoggingExceptionHandler = CoroutineExceptionHandler { _, t ->
Crashlytics.logException(t)
}
val AppScope = GlobalScope + LoggingExceptionHandler
复制代码
class ViewModelBase : ViewModel(), CoroutineScope {
override val coroutineContext = Job() + LoggingExceptionHandler
override fun onCleared() = coroutineContext.cancel()
}
复制代码
不是很糟糕
任什么时候候咱们必须处理边缘状况,事情每每会很快变得混乱。我但愿这篇文章可以帮助你了解在非标准条件下可能遇到的各类问题,以及你可使用的解决方案。
Happy Kotlining!
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。