koltin, какая разница от использования CoroutineScope напрямую и наследования класса от CoroutineScope

при запуске сопрограммы он может просто создать CoroutineScope и вызвать из него запуск {} - doSomething_2(),

или унаследовать класс от CoroutineScope и с классом для запуска {}. - 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()
    }
}

person lannyf    schedule 31.07.2020    source источник


Ответы (2)


Есть ли разница между этими двумя способами, какой из них предпочтительнее?

Да, есть фундаментальная разница, которая делает одно правильным, а другое неправильным. Речь идет о структурированном параллелизме: если ваш AClass является корневым объектом вашей единицы работы, чем бы она ни была, и отвечает (или наблюдателем) за ее жизненный цикл, то он также должен быть корневой областью для сопрограмм, которые вы ' Запущу в ней. Когда жизненный цикл заканчивается, AClass должен распространить это событие на подсистему сопрограмм, вызывая cancel на себе, отменяя корневую область. CoroutineScope.cancel - это функция расширения.

Я взял ваш код и внес следующие исправления:

  1. CoroutineScope.coroutineContext должен иметь Job() внутри, поэтому я добавил его. Я удалил диспетчер, потому что он не имеет отношения к этой истории, а диспетчер Main предназначен для графического интерфейса, в то время как мы проводим простой тест.

  2. Я удалил вашу dispose() функцию, у нас cancel() из коробки.

  3. Я удалил поля theJob1 и theJob2, потому что они бесполезны, если вы правильно начнете использовать структурированный параллелизм.

Я также добавил код, который позволит нам наблюдать за поведением:

  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) {
person Marko Topolnik    schedule 01.08.2020

Обе сопрограммы будут запущены с одинаковым контекстом. Вы можете увидеть это, распечатав контекст сопрограммы в обоих:

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 новую сопрограмму, существующий контекст просто комбинируется с новым контекстом, который вы предоставляете.

Дальнейшее чтение:

person Nicolas    schedule 01.08.2020