Как запустить несколько сопрограмм Kotlin параллельно и дождаться их завершения, прежде чем продолжить

Мне нужно запустить 2 сопрограммы параллельно и дождаться их завершения, прежде чем продолжить. Приведенный ниже код работает, но он использует GlobalScope, что не лучший способ сделать это.

Есть ли способ лучше?

fun getInfo(onSuccess: () -> Unit, onError: () -> Unit) {
        GlobalScope.launch(Dispatchers.IO) {
            try {
                coroutineScope {
                    launch { getOne() }
                    launch { getTwo() }
                }
                onSuccess.invoke()
            } catch (e: Throwable) {
                onError.invoke()
            }
        }
    }

person alexbtr    schedule 17.04.2020    source источник


Ответы (4)


Я бы предложил реализовать getInfo как функцию приостановки, которая знает контекст, в котором она должна работать. Таким образом, не имеет значения, из какого контекста вы его вызываете (*).

Кроме того, я бы не стал использовать обратные вызовы для продолжения после этого. Вы можете просто решить, что getInfo() вернет, как действовать (**).

На самом деле это самое лучшее в сопрограммах: вы можете превратить код, основанный на обратном вызове, в код, который читается как последовательный код.

Поскольку вам не важен результат getOne() и getTwo(), использование launch - правильный путь. Он возвращает Job. Вы можете приостановить сопрограмму до тех пор, пока обе функции не закончат работу с joinAll(), которая может быть вызвана на Collection<Job>.

suspend fun getInfo() = withContext(Dispatchers.IO) {
    try {
        listOf(
            launch { getOne() },
            launch { getTwo() }
        ).joinAll()
        false
    } catch (e: Throwable) {
        true
    }
}

Вам не нужно использовать GlobalScope, просто создайте свой (***).

Я использовал Default в качестве контекста для запуска getInfo, но любой другой контекст тоже подойдет, так как getInfo будет работать в том, что должен.

val newScope = CoroutineScope(Dispatchers.Default).launch {
    val error = getInfo()
    if(error) {
        onSuccess()
    } else {
        onError()
    }
}
// "newScope" can be cancelled any time

* На случай, если я использовал Dispatcher.IO, чтобы представить, что две функции выполняют какую-то длительную работу ввода-вывода.

** Здесь я использовал простое логическое значение, но, конечно, вы можете вернуть что-нибудь более значимое.

*** или подключитесь к некоторой области, заданной фреймворком sourrouding, который учитывает жизненный цикл

person Willi Mentzel    schedule 17.04.2020
comment
Спасибо, отличный ответ, и мне нравится ваша идея сделать его логическим. - person alexbtr; 17.04.2020
comment
Если вы создаете свой собственный CoroutineScope, обязательно отмените его в какой-то момент. В противном случае просто используйте GlobalScope. - person Dominic Fischer; 18.04.2020
comment
@DominicFischer GlobalScope также будет существовать, пока приложение не будет завершено. Итак, вам нужно позаботиться об интеграции ваших областей действия в жизненный цикл вашего приложения, но то, что это зависит от типа приложения, я даже упоминаю об этом или подключайтесь к некоторой области, заданной фреймворком sourrouding, который учитывает жизненный цикл - person Willi Mentzel; 26.04.2020

Вы можете использовать любой прицел, какой захотите. Это не влияет на ваш вопрос. GlobalScope не приветствуется, потому что он не инкапсулирует ваши задачи.

Какую область действия на самом деле использовать для запуска вашей сопрограммы, будет полностью зависеть от контекста того, что вы делаете, и от того, какую стратегию инкапсуляции вы используете.

Чтобы запустить несколько сопрограмм одновременно и дождаться их всех, вы используете async и await(). Вы можете использовать awaitAll() в их списке, если вам просто нужно закончить их все, прежде чем продолжить.

Кажется странным, что вы используете диспетчер ввода-вывода для выполнения своих обратных вызовов, но я оставлю это, потому что я слеп к контексту вашего кода.

fun getInfo(onSuccess: () -> Unit, onError: () -> Unit) {
        GlobalScope.launch(Dispatchers.IO) {
            try {
                coroutineScope {
                    listOf(
                        async { getOne() }
                        async { getTwo() }
                    }.awaitAll()
                }
                onSuccess.invoke()
            } catch (e: Throwable) {
                onError.invoke()
            }
        }
    }
person Tenfour04    schedule 17.04.2020

Вы можете создать CoroutineScope внутри класса, над которым вы работаете, и построить сопрограммы, просто вызвав launch builder. Что-то вроде этого:

class MyClass: CoroutineScope by CoroutineScope(Dispatchers.IO) { // or [by MainScope()]

    fun getInfo(onSuccess: () -> Unit, onError: () -> Unit) {
        launch {
            try {
                val jobA = launch { getOne() }
                val jobB = launch { getTwo() }
                joinAll(jobA, jobB)
                onSuccess.invoke()
            } catch (e: Throwable) {
                onError.invoke()
            }
        }
    }

    fun clear() { // Call this method properly
        this.cancel()
    }

}

Обязательно отмените любую сопрограмму, работающую в области видимости, правильно вызвав cancel.

Или, если вы работаете внутри ViewModel, просто используйте вместо этого viewModelScope.

person Glenn Sandoval    schedule 17.04.2020
comment
Я не верю, что ваш пример сработает, поскольку родительский CoroutineScope на самом деле не содержит задания. Вам нужно будет добавить его вручную в дополнение к Dispatchers.IO - person Kiskae; 17.04.2020
comment
@Kiskae На самом деле это работает, я создаю область, используя делегирование, как указано в официальной документации здесь. - person Glenn Sandoval; 17.04.2020
comment
@GlennSandoval, зачем обертывать вокруг него класс? Просто чтобы обзавестись собственным прицелом? Я думаю, это многовато. - person Willi Mentzel; 17.04.2020
comment
@WilliMentzel. Я предполагаю, что он работает внутри класса и ищет альтернативу GlobalScope.launch, поэтому я показываю, как создать область видимости для этого класса. Вот почему я посоветовал, если он работает внутри ViewModel, вместо этого ему следует использовать viewModelScope. - person Glenn Sandoval; 17.04.2020
comment
@GlennSandoval то, что вы предлагаете, правильно, но для этого случая слишком много. он просто делает слишком много (ненужных) предположений. мы не знаем, находится ли getInfo внутри класса и что мы имеем дело с приложением для Android, и вопрос не в этом. не поймите неправильно! - person Willi Mentzel; 17.04.2020

На самом деле есть даже лучший способ, основанный на принятом ответе:

suspend fun getInfo() = withContext(Dispatchers.IO) {
try {
    coroutineScope {
        launch { getOne() }
        launch { getTwo() }
    }
    false
} catch (e: Throwable) {
    true
}

}

person alexbtr    schedule 22.04.2020