kotlin - 在 Kotlin 中创建子协程范围
问题描述
简短的(-ish)故事
我想知道是否有或多或少的标准方法来创建协程上下文/范围,例如:
- 它是当前结构化并发协程的子进程,
- 它可以存储在某些属性等中,然后用于运行异步任务,例如
launch()
.
coroutineScope()完全符合我的需要,它创建了一个子作用域,但它并不简单地将其返回给调用者——我们需要传递一个 lambda,并且协程的生命周期仅限于这个 lambda 的执行。另一方面,CoroutineScope()工厂创建了一个长时间运行的范围,我可以存储它以供以后使用,但它与当前的协程无关。
我能够手动创建这样的范围:
suspend fun createChildCoroutineScope(): CoroutineScope {
val ctx = coroutineContext
return CoroutineScope(ctx + Job(ctx.job))
}
乍一看,它似乎完全符合我的需要。它是否相当于什么coroutineScope()
,或者我的解决方案可能不完整,我应该执行一些额外的任务?我试图阅读 的源代码coroutineScope()
,但它相当复杂。是否有更简单或更标准的方法来创建子范围?
此外,它是否被认为是一种不好的做法或反模式?我只是担心,如果还没有这么简单的函数,那么可能是有原因的,我不应该真的以这种方式使用协程。
用例(更长的故事)
通常,当我实现某种可以异步调度后台操作的长时间运行的服务时,我会发现需要这样做:
class MyService {
fun scheduleSomeTask() {
// start task in the background
// return immediately
}
}
使用协程有几种可能性:
GlobalScope
,但它很糟糕。使用当前协程使
scheduleSomeTask()
可暂停和运行后台任务。在许多情况下,我认为这种方法并不是真正合适的方法:- 后台任务由调用者“拥有”,而不是服务本身。如果我们停止服务,后台任务将仍在运行。
- 它要求调度功能是可挂起的。我认为这是错误的,因为我真的看不出为什么不允许某些 Java 代码或协程上下文之外的代码在我的服务中安排任务的原因。
给我的服务一个定义的生命周期,在停止/销毁时使用它创建
CoroutineScope()
范围cancel()
。这很好,但我认为我们仍然可以从协程的结构化并发中受益,所以对我来说,我的服务是分离的是一个缺点。例如,我们有一个文件下载服务,它由(拥有)其他服务组成,包括数据缓存服务。
start()
使用/服务的典型方法,stop()
我们需要手动控制生命周期,并且很难正确处理故障。协程让它变得更容易:如果缓存服务崩溃,它会自动传播到下载服务;如果下载服务需要停止,它只是取消它的协程,它可以确保它不会泄漏它的任何子组件。所以对我来说,在设计一个由几个小服务组成的应用程序时,协程的结构化并发可能非常有用。
我目前的方法是这样的:
class MyService {
private lateinit var coroutine : CoroutineScope
suspend fun start() {
coroutine = createChildCoroutineScope() + CoroutineName("MyService")
}
fun stop() {
coroutine.cancel()
}
fun scheduleSomeTask() {
coroutine.launch {
// do something
}
}
}
或者,或者:
class MyService(
private val coroutine: CoroutineScope
) {
companion object {
suspend fun start() = MyService(createChildCoroutineScope())
}
}
这样,服务“拦截”启动它的协程并将其后台操作附加到它。但正如我所说,我不确定这是否由于某种原因不被视为反模式。
另外,我知道我createChildCoroutineScope()
的有潜在危险。通过调用它,我们使当前的协程无法完成。这可能是库中不存在此类函数的原因。另一方面,它与执行以下操作并没有什么不同:
launch {
while (true) {
socket.accept() // assume it is suspendable, not blocking
// launch connection handler
}
}
事实上,从技术角度来看,这两种方法都非常相似。它们具有相似的并发结构,但我相信“我的”方法通常更干净、更强大。
解决方案
我为我的问题找到了一个非常好的答案和解释。Roman Elizarov 在他的一篇文章中准确地讨论了我的问题:https ://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055
他解释说,虽然在技术上可以“捕获”挂起函数的当前上下文并使用它来启动后台协程,但强烈建议不要这样做:
不要这样做!它使启动协程的范围变得不透明和隐含,捕获一些外部
Job
以启动新的协程,而无需在函数签名中明确宣布它。协程是与您的其余代码同时进行的一项工作,并且它的启动必须是明确的。如果您需要在函数返回后启动一个继续运行的协程,则将您的函数作为参数的扩展
CoroutineScope
或scope: CoroutineScope
作为参数传递,以在函数签名中明确您的意图。不要让这些功能暂停。
我知道我可以只将CoroutineScope
/传递CoroutineContext
给函数,但我认为挂起函数会是一种更短、更优雅的方法。然而,上面的解释让地狱很有意义。如果我们的函数需要获取调用者的协程范围/上下文,请明确说明这一点 - 它再简单不过了。
这也与“热”/“冷”执行的概念有关。挂起函数的一大优点是它们允许我们轻松创建长时间运行任务的“冷”实现。虽然我相信 coroutines 文档中没有明确规定挂起函数应该是“冷的”,但通常满足这个要求是一个好主意,因为我们的挂起函数的调用者可能会认为它是“冷的”。捕获协程上下文会使我们的函数“热”,因此应该通知调用者。
推荐阅读
- python - insert + upsert 在 sqlite3 控制台中有效,但在 python 上显示语法错误
- apache-spark - 我可以通过“预拆分”区域服务器避免 Habse 表中的“热点”问题吗?
- css - SCSS 为背景生成错误的 url
- python - python:如何将多个列表附加到一个
- angular - 'child-selector' 不是已知元素 Angular 2
- oracle-data-integrator - 使用 Oracle Data Integrator 将数据泵加载到表时出现问题
- regression - 使用 Accord.net 求解对数函数
- vue.js - 如何在 VueJS 中渲染模板以用于 index.html
- python - 在系列对象之间提取字符串
- javascript - Node.JS .listen(port, 'hostname') 不起作用