koltin,与直接使用 CoroutineScope 并从 CoroutineScope 派生 class 有什么区别
koltin, what's difference from using CoroutineScope directly and derive the class from CoroutineScope
启动协程时,它可能只是创建一个 CoroutineScope
并从中调用启动{}——doSomething_2()
、
或从 CoroutineScope
派生 class 并使用 class 启动{}。 -- doSomething_1()
.
这两种方式有区别吗?
class AClass : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
var theJob1: Job? = null
var theJob2: Job? = null
fun doSomething_1() {
theJob1 = launch(Dispatchers.IO) {
// ... ...
}
}
fun doSomething_2() {
theJob2 = CoroutineScope(Dispatchers.IO).launch {
// ... ...
}
}
fun dispose() {
theJob1?.cancel()
theJob2?.cancel()
}
}
两个协程将在相同的上下文中启动。您可以通过在两者中打印协程上下文来看到这一点:
launch(Dispatchers.IO) {
println("doSomething_1 context: ${coroutineContext}")
}
CoroutineScope(Dispatchers.IO).launch {
println("doSomething_2 context: ${coroutineContext}")
}
这将打印如下内容:
doSomething_1 context: [StandaloneCoroutine{Active}@7b8cce78, Dispatchers.IO]
doSomething_2 context: [StandaloneCoroutine{Active}@3c938006, Dispatchers.IO]
我还没有看到 CoroutineScope
经常在内部协程代码之外实现。在这种情况下,您应该更喜欢组合而不是继承,特别是因为 CoroutineContext
的核心是可组合的,使用 +
运算符。例如,当您 launch
一个新的协程时,现有上下文只是与您提供的新上下文相结合。
进一步阅读:
- Kotlin: Coroutines scope vs Coroutine context
- Why your class probably shouldn’t implement CoroutineScope
Is there difference between these two, which way is preferred?
是的,有一个根本的区别可以使一个正确而另一个不正确。它是关于结构化并发的:如果你的 AClass
是你的“工作单元”的根对象,无论它是什么,并且负责(或观察者)它的生命周期,那么它也应该是根您将在其中启动的协程的范围。当生命周期结束时,AClass
应该通过调用 cancel
自身来将该事件传播到协程子系统,从而取消根作用域。 CoroutineScope.cancel
是一个扩展函数。
我使用了您的代码并进行了以下修复:
CoroutineScope.coroutineContext
里面一定有一个 Job()
所以我加了它。我删除了调度程序,因为它与本文无关,并且 Main
调度程序用于 GUI,而我们正在 运行 进行简单测试。
我删除了你的 dispose()
功能,我们有 cancel()
开箱即用。
我删除了 theJob1
和 theJob2
字段,因为一旦您开始正确使用结构化并发,它们就没有用了。
我还添加了一些代码,可以让我们观察行为:
在每个协程中添加了一个 delay
和一个 println
以查看何时完成。
添加了一个main
函数来测试它。该函数在最后一行永远阻塞,以便我们可以看到启动的协程将做什么。
代码如下:
import kotlinx.coroutines.*
import java.lang.Thread.currentThread
import kotlin.coroutines.CoroutineContext
fun main() {
val a = AClass()
a.doSomething_1()
a.doSomething_2()
a.cancel()
currentThread().join()
}
class AClass : CoroutineScope {
override val coroutineContext: CoroutineContext = Job()
fun doSomething_1() {
launch(Dispatchers.IO) {
try {
delay(10_000)
} finally {
println("theJob1 completing")
}
}
}
fun doSomething_2() {
CoroutineScope(Dispatchers.IO).launch {
try {
delay(10_000)
} finally {
println("theJob2 completing")
}
}
}
}
当你 运行 它时,你只会看到 theJob1
完成,而 theJob2
运行 整整 10 秒,不遵守 cancel
信号。
这是因为构造 CoroutineScope(Dispatchers.IO)
创建了一个独立的作用域,而不是成为 AClass
作用域的子作用域,从而破坏了协程层次结构。
从理论上讲,您仍然可以使用显式 CoroutineScope
构造函数来保持层次结构,但那样一来您就会得到一些显然不是首选方式的东西:
CoroutineScope(coroutineContext + Dispatchers.IO).launch {
这相当于
launch(Dispatchers.IO) {
启动协程时,它可能只是创建一个 CoroutineScope
并从中调用启动{}——doSomething_2()
、
或从 CoroutineScope
派生 class 并使用 class 启动{}。 -- doSomething_1()
.
这两种方式有区别吗?
class AClass : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
var theJob1: Job? = null
var theJob2: Job? = null
fun doSomething_1() {
theJob1 = launch(Dispatchers.IO) {
// ... ...
}
}
fun doSomething_2() {
theJob2 = CoroutineScope(Dispatchers.IO).launch {
// ... ...
}
}
fun dispose() {
theJob1?.cancel()
theJob2?.cancel()
}
}
两个协程将在相同的上下文中启动。您可以通过在两者中打印协程上下文来看到这一点:
launch(Dispatchers.IO) {
println("doSomething_1 context: ${coroutineContext}")
}
CoroutineScope(Dispatchers.IO).launch {
println("doSomething_2 context: ${coroutineContext}")
}
这将打印如下内容:
doSomething_1 context: [StandaloneCoroutine{Active}@7b8cce78, Dispatchers.IO]
doSomething_2 context: [StandaloneCoroutine{Active}@3c938006, Dispatchers.IO]
我还没有看到 CoroutineScope
经常在内部协程代码之外实现。在这种情况下,您应该更喜欢组合而不是继承,特别是因为 CoroutineContext
的核心是可组合的,使用 +
运算符。例如,当您 launch
一个新的协程时,现有上下文只是与您提供的新上下文相结合。
进一步阅读:
- Kotlin: Coroutines scope vs Coroutine context
- Why your class probably shouldn’t implement CoroutineScope
Is there difference between these two, which way is preferred?
是的,有一个根本的区别可以使一个正确而另一个不正确。它是关于结构化并发的:如果你的 AClass
是你的“工作单元”的根对象,无论它是什么,并且负责(或观察者)它的生命周期,那么它也应该是根您将在其中启动的协程的范围。当生命周期结束时,AClass
应该通过调用 cancel
自身来将该事件传播到协程子系统,从而取消根作用域。 CoroutineScope.cancel
是一个扩展函数。
我使用了您的代码并进行了以下修复:
CoroutineScope.coroutineContext
里面一定有一个Job()
所以我加了它。我删除了调度程序,因为它与本文无关,并且Main
调度程序用于 GUI,而我们正在 运行 进行简单测试。我删除了你的
dispose()
功能,我们有cancel()
开箱即用。我删除了
theJob1
和theJob2
字段,因为一旦您开始正确使用结构化并发,它们就没有用了。
我还添加了一些代码,可以让我们观察行为:
在每个协程中添加了一个
delay
和一个println
以查看何时完成。添加了一个
main
函数来测试它。该函数在最后一行永远阻塞,以便我们可以看到启动的协程将做什么。
代码如下:
import kotlinx.coroutines.*
import java.lang.Thread.currentThread
import kotlin.coroutines.CoroutineContext
fun main() {
val a = AClass()
a.doSomething_1()
a.doSomething_2()
a.cancel()
currentThread().join()
}
class AClass : CoroutineScope {
override val coroutineContext: CoroutineContext = Job()
fun doSomething_1() {
launch(Dispatchers.IO) {
try {
delay(10_000)
} finally {
println("theJob1 completing")
}
}
}
fun doSomething_2() {
CoroutineScope(Dispatchers.IO).launch {
try {
delay(10_000)
} finally {
println("theJob2 completing")
}
}
}
}
当你 运行 它时,你只会看到 theJob1
完成,而 theJob2
运行 整整 10 秒,不遵守 cancel
信号。
这是因为构造 CoroutineScope(Dispatchers.IO)
创建了一个独立的作用域,而不是成为 AClass
作用域的子作用域,从而破坏了协程层次结构。
从理论上讲,您仍然可以使用显式 CoroutineScope
构造函数来保持层次结构,但那样一来您就会得到一些显然不是首选方式的东西:
CoroutineScope(coroutineContext + Dispatchers.IO).launch {
这相当于
launch(Dispatchers.IO) {