Изучение основ сопрограмм: часть 1

Изучение основ сопрограмм: часть 1

17 февраля 2023 г.

Вы прочитали 20 статей, посмотрели 6 видео, спросили об этом коллег и почему-то до сих пор не можете понять, как работают корутины; вы, вероятно, думаете, что выучить корутины невозможно. Я чувствовал то же самое до того, как начал писать эту статью.

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

Он ответит на такие вопросы, как «В чем разница между CoroutineScope и CoroutineContext», «Нужно ли мне переключить диспетчер?», «Что такое структурированный параллелизм?», «Как справиться с отменой?» и т. д.

Я не буду вдаваться в подробности о том, как работают корутины, но о том, как правильно их использовать. Эта статья в основном посвящена Coroutines для Android, но большинство понятий применимы и к Kotlin в целом.

Что такое сопрограммы?

Сопрограммы Kotlin представили новый стиль параллелизма, который можно использовать для упрощения асинхронного кода. Если вы использовали обратные вызовы в прошлом, вы, возможно, знаете, насколько грязными могут быть вещи, для этого даже есть название «Ад обратных вызовов».

Помимо упрощения написания асинхронного кода, kotlinx.coroutines также содержит несколько модулей, которые помогут вам в других вещах, таких как написание реактивный код.

Photo by Ahmed zid on Unsplash

Зачем использовать сопрограммы?

Если вы используете Kotlin на сервере, вы, вероятно, используете Ktor, и он работает асинхронно с помощью Coroutines. Большинство его функций приостановлено, поэтому вам нужно написать приостановленный код, чтобы воспользоваться этим.

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

Этих проблем можно легко избежать, используя сопрограммы и написав основные безопасные функции.

Почему бы просто не использовать RxJava?

Большинство людей используют RxJava в течение многих лет, и перенос приложения на Coroutines — непростая задача. Настоящая сила RxJava — это реактивное программирование и обратное давление. Если вы используете его для управления асинхронными запросами, вы фактически используете базуку, чтобы убить паука.

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

Приостановить

Модификатор suspend — центральная часть сопрограмм. Приостанавливающая функция — это просто функция, которую можно приостановить и возобновить позже.

suspend fun yourFunction() {
  // code
}

Функции приостановки позволяют вам приостановить выполнение текущей сопрограммы, не блокируя поток. Это означает, что рассматриваемый код может приостанавливать выполнение при вызове приостанавливающей функции и возобновлять выполнение позже.

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

Зачем вам вообще беспокоиться об этом, если современные операционные системы поддерживают несколько потоков?

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

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

Главная безопасность с сопрограммами

Одним из распространенных заблуждений является то, что добавление модификатора приостановки к функции делает ее либо асинхронной, либо неблокирующей. Suspend не указывает Kotlin выполнять функцию в фоновом потоке. Приостанавливающие функции являются асинхронными только в том случае, если они используются явно как таковые.

Сопрограммы могут выполняться в основном потоке, например, при запуске сопрограммы в ответ на событие пользовательского интерфейса; это нормально, потому что они не блокируют.

Практическое правило заключается в том, что приостанавливающие функции никогда не блокируют вызывающий поток, что делает их безопасными для основной среды.

Контекст сопрограммы

CoroutineContext — это набор элементов, определяющих поведение сопрограммы. Это означает, что вы можете изменить поведение сопрограммы, добавив элементы в CoroutineContext. Он состоит из:

* Задание — управляет жизненным циклом сопрограммы.

* CoroutineDispatcher — определяет поток, в который будет отправлена ​​работа.

* CoroutineExceptionHandler — обрабатывает неперехваченные исключения.

* CoroutineName — Добавляет имя сопрограмме (полезно для отладки).

Контексты CoroutineContext можно комбинировать с помощью оператора +, результатом будет новый ContextCoroutineContext. Уже существующие элементы перезаписываются.

Использование одного и того же типа элемента при объединении контекстов вызывает исключение во время выполнения.

Dispatchers.Main + Dispatchers.IO
// Using 'plus(CoroutineDispatcher): CoroutineDispatcher' is an error. Operator '+' on two CoroutineDispatcher objects is meaningless.

Область сопрограммы

CoroutineScope отслеживает все созданные им сопрограммы. Следовательно, если вы отмените область действия, вы отмените все созданные ею сопрограммы. Текущую работу (запуск сопрограмм) можно отменить, вызвав scope.cancel() в любой момент времени.

Это не означает, что сопрограммы перестанут работать, как только вы вызовете отмену; код сопрограммы должен взаимодействовать, чтобы его можно было отменить. Мы рассмотрим, как это сделать позже.

Вы должны создавать CoroutineScope каждый раз, когда хотите запускать и контролировать жизненный цикл сопрограмм на определенном уровне вашего приложения.

val scope = CoroutineScope(Dispatchers.Default)

В Android есть библиотеки KTX, которые уже предоставляют CoroutineScope в определенных классах жизненного цикла, таких как viewModelScope и lifecycleScope.

Если ваша ViewModel будет уничтожена, вся выполняемая асинхронная работа будет остановлена. Таким образом, вы не тратите ресурсы впустую.

<цитата>

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

Мануэль Виво

Для объектов жизненного цикла это очень похоже

class CarFragment : Fragment {
  init {
    lifecycleScope.launch {
      // You can run a suspend function here, it'll be canceled when the Lifecycle is destroyed
    }
  }        
}

Диспетчер сопрограмм

CoroutineDispatcher отвечает за диспетчеризацию выполнения сопрограммы в потоке.

Kotlin предоставляет следующие реализации:

* Dispatchers.Default — его используют все обычные сборщики. Он использует преимущества пула общих фоновых потоков. Это хороший вариант для ресурсоемких сопрограмм, которым требуются ресурсы ЦП.

* Dispatchers.IO — предназначен для разгрузки блокирующих задач с интенсивным вводом-выводом (таких как ввод-вывод файлов и блокировка ввода-вывода сокетов) с помощью общего пула потоков, генерируемых по требованию.

* Dispatchers.Unconfined — запускает выполнение сопрограммы в текущем кадре вызова и продолжается до первой приостановки, после чего возвращается метод конструктора сопрограммы.

Сопрограмма впоследствии перезапустится в том потоке, который был использован связанной функцией приостановки, без привязки к какому-либо конкретному потоку или пулу. В большинстве случаев диспетчер Unconfined не следует использовать в коде.

Вы также можете выполнять сопрограммы в любом из ваших пулов потоков, преобразовав их в CoroutineDispatcher с помощью функции расширения Executor.asCoroutineDispatcher(). Пулы частных потоков можно создавать с помощью newSingleThreadContext и newFixedThreadPoolContext .

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

* Комната обеспечивает основную безопасность автоматически, если вы используете функции приостановки, RxJava или LiveData.

* Retrofit и Volley управляют своими собственными потоками и не требуют явной основной безопасности в вашем коде при использовании с сопрограммами Kotlin.

Переключение тем

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

withContext(Dispatchers.IO) {
  yourFunction()
}

Поскольку withContext позволяет вам контролировать, в каком потоке выполняется любая строка кода, без введения обратного вызова для возврата результата, вы можете применять его к очень небольшим функциям, таким как чтение из вашей базы данных или выполнение сетевого запроса (только что если библиотека, которую вы используете, этого не делает).

Таким образом, хорошей практикой является использование withContext, чтобы убедиться, что каждая функция может быть безопасно вызвана в любом Dispatcher, включая Main — таким образом вызывающему объекту никогда не придется думать о том, какой поток потребуется для выполнения функции.

Производительность withContext

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

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

CoroutineScheduler, который является Пул потоков, используемый по умолчанию в реализации JVM, распределяет отправленные сопрограммы по рабочим потокам наиболее эффективным способом.

Поскольку Dispatchers.Default и Dispatchers.IO используют один и тот же пул потоков, перемещение между ними упрощается, чтобы предотвратить изменение потоков, где это возможно.

Библиотека сопрограмм даже оптимизирует эти вызовы< /a>, оставаясь в том же диспетчере, используя быстрый путь.

В этой статье мы узнали о 4 строительных блоках сопрограмм: suspend, CoroutineCoxtext, CoroutineScope и CoroutineDispatcher.

Одно только знание о них не позволяет вам многое сделать; вот почему мы собираемся научиться использовать их в реальном коде во второй части, изучая Job, SupervisorJob, launch и асинхронный.

Сопрограммы — непростая тема, поэтому не волнуйтесь, если вы не поняли большую часть прочитанного; вернитесь к началу и прочтите еще раз. Я также рекомендую вам прочитать следующие статьи:

* Kotlin Coroutines: функция приостановки * Корутины на Android (часть I): получение фона * CoroutineDispatcher * Простые сопрограммы в Android: viewModelScope * Руководство по программированию пользовательского интерфейса с помощью сопрограмм

Если у вас есть какие-либо сомнения, не стесняйтесь обращаться ко мне. Увидимся в следующей статье 😉


Фото на обложке Келли Сиккема на Unsplash


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE