У меня возникла проблема с сопрограммой Kotlin. Когда я запускаю функцию приостановки performTask
, я замечаю, что сначала печатается сообщение журнала «Шаг 1: Задача внутри withContext», и только через 10 секунд печатается сообщение «Шаг 2: Задача вне withContext».
Вот код:
suspend fun weirdFunction() {
CoroutineScope(coroutineContext).launch {
delay(10_000)
}
}
suspend fun performTask() {
withContext(Dispatchers.Default) {
weirdFunction()
Log.d("MyTag", "Step 1: Task within withContext")
}
Log.d("MyTag", "Step 2: Task outside withContext")
}
Я считаю, что проблема вызвана использованием CoroutineScope(coroutineContext).launch
внутри weirdFunction
, но я не понимаю, почему такое использование приводит к такому поведению. Мое намерение состояло в том, чтобы запустить дочернюю сопрограмму в текущей области действия сопрограммы, не передавая ее в качестве параметра. Я подумал, что использование coroutineContext
для получения текущей области действия сопрограммы — элегантный способ добиться этого.
Однако, похоже, мое понимание может быть неверным. Эта проблема заставила меня задуматься о более тревожном сценарии: если бы weirdFunction
был частью сторонней библиотеки, это могло бы привести к зависанию withContext
и не возвращению, как ожидалось, что очень затруднило бы отладку.
Может ли кто-нибудь объяснить, почему этот подход заставляет блок withContext
ждать завершения запущенной сопрограммы? Есть ли лучший способ запустить неблокирующую сопрограмму внутри функции приостановки, которая не влияет на область действия сопрограммы вызывающего абонента? Будем очень признательны за любые идеи или объяснения.
@TimRoberts Если использовать CoroutineScope(Dispatchers.Default).launch { ... }
, withContext
не будет ждать.
«Я думал, что использование coroutineContext для получения текущей области действия сопрограммы — элегантный способ добиться этого». — на самом деле это антипаттерн в сопрограммах. Смотрите мой вопрос, мой ответ и особенно связанную статью Романа Елизарова: stackoverflow.com/questions/67749075/…
Когда вы запускаете performTask
, происходит следующее:
withContext
создает новое задание для синхронного block
выполнения и ждет его завершения. Назовем эту работу job1. Не имеет значения, что вы передаете в качестве параметра withContext
.weirdFunction
и создается новая область сопрограммы. Вам всегда необходимо связать область сопрограммы с заданием. Если вы не предоставите его, под капотом будет создан новый. Однако в данном случае вы предоставили работу, поскольку job1 является частью coroutineContext
.weirdFunction
возвращается и регистрируется «Шаг 1: Задача внутри withContext».withContext
готов... почти! Задание 1, которое использовалось для запуска блока, все еще выполняется, поскольку один из его дочерних элементов все еще работает: задание 2 еще не завершено, оно все еще ждет 10 секунд. Таким образом, задание job1 также выполняется и withContext
вернется только после его завершения.withContext
возвращается и регистрируется «Шаг 2: Задача снаружи с контекстом».Если бы вы не присоединили job1 к новой области сопрограммы, вы бы добились желаемого поведения:
CoroutineScope(Job()).launch {
delay(10_000)
}
Однако функции приостановки не должны пытаться запускать новые сопрограммы, которые переживут саму функцию. Причина этого в том, что у вас не будет возможности управлять этой сопрограммой, например, отменить ее, когда она больше не нужна.
Если вам нужно запустить новую сопрограмму, вам следует передать экземпляр CoroutineScope
в качестве параметра. Внутри функции приостановки его можно использовать для запуска новой сопрограммы, а вне функции приостановки — для отмены сопрограммы. Вы также можете указать параметр в качестве получателя функции расширения, например:
fun CoroutineScope.weirdFunction() {
launch {
delay(10_000)
}
}
Обратите внимание, что это больше даже не функция приостановки, поскольку все, что связано с сопрограммой, применяется к области действия сопрограммы.
Это делает поведение функции более прозрачным: это не функция приостановки, поэтому вам не нужна сопрограмма для ее вызова, но вам необходимо предоставить область действия сопрограммы.
С другой стороны, от функции приостановки ожидают, что она выполнит то, что ей нужно; между ними она может приостанавливаться, но когда эта функция возвращается, она также завершает то, что намеревалась сделать, и не остается никакой висящей сопрограммы, над которой вы не могли бы иметь никакого контроля.
Когда вы вызываете эту новую weirdFunction
из withContext
, она будет демонстрировать то же поведение, что и раньше, но этого можно ожидать, просто взглянув на сигнатуру функции, потому что вы знаете, что передаете текущую область сопрограммы с контекстом withContext
(и ее заданием), поэтому, что бы ни происходило внутри weirdFunction
, ясно, что withContext
будет ждать, пока это закончится.
Если это нежелательное поведение, вы всегда можете предоставить другую область действия сопрограммы, не зависящую от withContext
:
scope.weirdFunction()
Просто не забудьте сохранить ссылку на эту область действия, чтобы отменить ее, когда она больше не понадобится.
Большое спасибо за подробное объяснение! Ваш ответ прояснил поведение withContext
и то, как в нем работает иерархия должностей. Предоставленные вами примеры и предложения были невероятно полезны. Теперь я гораздо лучше понимаю, как правильно управлять сопрограммами и их областями действия. Еще раз спасибо за ценную информацию!
withContext
ждет, пока все процедуры в блоке завершатся.