原文地址:Advanced Kotlin Coroutines tips and tricks原文作者:Alex Saveau译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/coroutines-snags.md译者:nanjingboy校对者:zx-Zhu
原文地址:Advanced Kotlin Coroutines tips and tricks
本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/coroutines-snags.md
协程从 1.3 开始成为稳定版!
开始 Kotlin 协程非常简单:只需将一些耗时操作放在 launch 中即可,你做到了,对不?当然,这是针对简单的情况。但很快,并发与并行的复杂性会慢慢堆积起来。
launch
当你深入研究协程时,以下是一些你需要知道的事情。
没有办法绕过它:在某些时候,你不得不用原生 Java 流。这里的问题(很多情况下 ??)是使用流将会堵塞当前线程。这在协程中是一个坏消息。现在,如果你想要取消一个协程,在能够继续执行之前,你不得不等待读写操作完成。
作为一个简单可重复的例子,让我们打开 ServerSocket 并且等待 1 秒的超时连接:
ServerSocket
runBlocking(Dispatchers.IO) { withTimeout(1000) { val socket = ServerSocket(42)
// 我们将卡在这里直到有人接收该连接。难道你不想知道为什么吗??? socket.accept() } }
应该可以运行,对吗?不。
现在你的感受有点像:??。 那么我们如何解决呢?
当 Closeable APIs 构建良好时,它们支持从任何线程关闭流并适当地失败。
Closeable
注意:通常情况下,JDK 中的 APIs 遵循了这些最佳实践,但需注意第三方 Closeable APIs 可能并没有遵循。 你被提醒过了。
幸亏 suspendCancellableCoroutine 函数,当一个协程被取消时我们可以关闭任何流:
suspendCancellableCoroutine
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 包裹,该协程会在超时触发的时候失败。
accept
useCancellably
// 抛出 `SocketException: socket closed` 异常。好极了! socket.useCancellably { it.accept() } } }
成功!
如果你不支持取消怎么办?以下是你需要注意的事项:
onDestroy
ViewModel
Dispatchers.IO
suspendCoroutine
async
由于上面关于这两个特性的回答已经过时,我想我会再次分析它们的差异。
当一个协程崩溃时,它的父节点将被取消,从而取消所有父节点的子节点。一旦整个树节点中的协程完成取消操作,异常将会发送到当前上线文的异常处理程序。在 Android 中,这意味著 你的 程序将会 崩溃,而不管你使用什么来进行调度。
这意味著 await() 显式处理所有异常,安装 CoroutineExceptionHandler 将无任何效果。
await()
CoroutineExceptionHandler
虽然该函数会立即返回,但其父作用域将 不会 结束,直到使用 launch 构建的所有协程以某种方式完成。因此如果你只是想等待所有协程完成,在父作用域末尾调用所有子作业的 join() 就没有必要了。
join()
与你期望的可能不同,即使未调用 await(),外部作用域仍将等待async协程完成。
await()
这一部分相当简单:如果你需要协程的返回值,async 是唯一的选择。如果你不需要返回值,使用 launch 来创建副作用。并且在继续执行之前需要完成这些副作用才需要使用 join()。
join() 在 await() 时 不会 重新抛出异常。但如果发生错误,join() 会取消你的协程,这意味著在 join() 挂起后调用任何代码都不会起作用。
现在你了解了你所使用不同构造器异常处理机制的差异,你会陷入两难境地:你想记录异常而不崩溃(所以我们不能使用 launch),但是你不想手动调用 try/catch (所以我们不能使用 async)。所以这让我们无所适从?谢天谢地。
try
catch
记录异常是 CoroutineExceptionHandler 派上用场的地方。但首先,让我们花点时间了解在协程中抛出异常时究竟发生了什么:
Continuation
CancellationException
CoroutineContext
ServiceLoader
考虑到这一点,我们有以下几个选择:
GlobalScope
最后一个方案是所推荐的,因为它具有灵活性并且需要最少的代码和技巧。
对于应用程序范围内的作业,你将使用带有日志记录处理程序的 AppScope。对于其他业务,你可以在日志记录崩溃的适当位置添加处理程序。
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、前端、后端、区块链、产品、设计、人工智慧等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。
推荐阅读: