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 一个新的协程时,现有上下文只是与您提供的新上下文相结合。

进一步阅读:

Is there difference between these two, which way is preferred?

是的,有一个根本的区别可以使一个正确而另一个不正确。它是关于结构化并发的:如果你的 AClass 是你的“工作单元”的根对象,无论它是什么,并且负责(或观察者)它的生命周期,那么它也应该是根您将在其中启动的协程的范围。当生命周期结束时,AClass 应该通过调用 cancel 自身来将该事件传播到协程子系统,从而取消根作用域。 CoroutineScope.cancel 是一个扩展函数。

我使用了您的代码并进行了以下修复:

  1. CoroutineScope.coroutineContext 里面一定有一个 Job() 所以我加了它。我删除了调度程序,因为它与本文无关,并且 Main 调度程序用于 GUI,而我们正在 运行 进行简单测试。

  2. 我删除了你的 dispose() 功能,我们有 cancel() 开箱即用。

  3. 我删除了 theJob1theJob2 字段,因为一旦您开始正确使用结构化并发,它们就没有用了。

我还添加了一些代码,可以让我们观察行为:

  1. 在每个协程中添加了一个 delay 和一个 println 以查看何时完成。

  2. 添加了一个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) {