Почему мой блок withContext ожидает запуска сопрограммы в функции приостановки?

У меня возникла проблема с сопрограммой 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 ждать завершения запущенной сопрограммы? Есть ли лучший способ запустить неблокирующую сопрограмму внутри функции приостановки, которая не влияет на область действия сопрограммы вызывающего абонента? Будем очень признательны за любые идеи или объяснения.

withContext ждет, пока все процедуры в блоке завершатся.
Tim Roberts 15.06.2024 20:25

@TimRoberts Если использовать CoroutineScope(Dispatchers.Default).launch { ... }, withContext не будет ждать.

luxi78 16.06.2024 04:40

«Я думал, что использование coroutineContext для получения текущей области действия сопрограммы — элегантный способ добиться этого». — на самом деле это антипаттерн в сопрограммах. Смотрите мой вопрос, мой ответ и особенно связанную статью Романа Елизарова: stackoverflow.com/questions/67749075/…

broot 16.06.2024 15:39
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Когда вы запускаете performTask, происходит следующее:

  1. withContext создает новое задание для синхронного block выполнения и ждет его завершения. Назовем эту работу job1. Не имеет значения, что вы передаете в качестве параметра withContext.
  2. Теперь вызывается weirdFunction и создается новая область сопрограммы. Вам всегда необходимо связать область сопрограммы с заданием. Если вы не предоставите его, под капотом будет создан новый. Однако в данном случае вы предоставили работу, поскольку job1 является частью coroutineContext.
  3. Новая область используется для запуска новой сопрограммы. При этом создается новое задание, которое можно использовать для управления этой сопрограммой. Назовем эту работу job2. Его родительское задание берется из области сопрограммы, то есть job1.
  4. Пока новая сопрограмма ждет 10 секунд, weirdFunction возвращается и регистрируется «Шаг 1: Задача внутри withContext».
  5. Вот и блок withContext готов... почти! Задание 1, которое использовалось для запуска блока, все еще выполняется, поскольку один из его дочерних элементов все еще работает: задание 2 еще не завершено, оно все еще ждет 10 секунд. Таким образом, задание job1 также выполняется и withContext вернется только после его завершения.
  6. Через 10 секунд задание2 завершается, а вместе с ним завершается и задание1. 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 и то, как в нем работает иерархия должностей. Предоставленные вами примеры и предложения были невероятно полезны. Теперь я гораздо лучше понимаю, как правильно управлять сопрограммами и их областями действия. Еще раз спасибо за ценную информацию!

luxi78 16.06.2024 08:38

Другие вопросы по теме