Рубрики
Без рубрики

Все, что вам нужно знать о сопрограммах kotlin

Все, что вам нужно знать о сопрограммах kotlin Моя повестка дня для этого поста в блоге состоит в том, чтобы получить… С тегами kotlin, сопрограммы, параллелизм, java.

Моя повестка дня для этого поста в блоге состоит в том, чтобы ознакомиться с различными терминологиями сопрограмм и ответить на следующие вопросы:

  • Разница между Работой и Отложенный , запуск и асинхронный

  • Какой конструктор сопрограмм вы должны использовать?

  • Что происходит, когда в сопрограмме возникает исключение ?

  • На очень высоком уровне, как достигается структурированный параллелизм ?

  • Потокобезопасное общее изменяемое состояние с однопоточным диспетчером

  • Ввод-вывод и процессор связанные операции с сопрограммами

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

В остальной части статьи мы рассмотрим следующие темы:

  • Строители сопрограммной области ((Основная область, Область сопрограммы, область сопрограммы, Глобальная область и т.д.)

  • Контекст сопрограммы (Работа, Отложенная и т.д.)

  • Построители сопрограмм (запуск, асинхронность, с контекстом, блокировка запуска и т.д.)

  • Структурированный параллелизм

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

  • Продолжение

Вступление

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

Двумя наиболее важными строительными блоками для создания/запуска/запуска новых сопрограмм являются область применения сопрограммы и строители сопрограмм .

Область действия сопрограммы состоит из всего оборудования, необходимого для запуска сопрограммы, например, она знает, где (в каком потоке) запускать сопрограмму, и конструкторы сопрограмм используются для создания новой сопрограммы.

Если мне нужно провести аналогию с потоками, область действия сопрограммы можно рассматривать как Java ExecutorService , а разработчики сопрограмм – это фабрики для создания Запускаемые экземпляры.

Область применения сопрограммы – это больше, чем просто Служба исполнителей !

Чтобы продемонстрировать, что сопрограммы легкие , давайте рассмотрим следующие примеры, в которых мы создаем 100 тысяч сопрограмм против 100 тысяч потоков и печатаем (“.”) в каждой из них.

100_000 сопрограмм:

Вышеупомянутое приложение не аварийно завершает работу и печатает 100_000 точек.

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

100_000 потоков:

Выше приложение завершает работу с java.lang. Ошибка OutOfMemoryError: не удалось создать новый собственный поток

Строители области сопрограммирования

Область действия сопрограммы – это интерфейс, который имеет одно абстрактное свойство, называемое контекстом сопрограммы.

    public interface CoroutineScope {
        public val coroutineContext: CoroutineContext
    }

Область действия сопрограммы – это не что иное, как контекст сопрограммы, единственная разница заключается в их предполагаемом использовании. Роман Елизаров подробно объясняет это в следующем посте в блоге

Контекст и область применения сопрограммы. Сопрограммы Котлина имеют контекст. Там… |автор Роман Елизаров | Средний

Роман Елизаров ・ 9 марта 2019 г. ・ 4 мин Среда чтения

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

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

Как создать область сопрограммы

Основная область применения

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

    public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Примечание : Для работы с Основным диспетчером необходимо добавить в проект дополнительные зависимости среды выполнения, зависящие от платформы:

  • kotlinx-сопрограммы-android—для диспетчера основных потоков Android

  • kotlinx-сопрограммы-javafx— для диспетчера потоков приложений JavaFX

  • kotlinx-сопрограммы-swing—для диспетчера Swing EDT

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

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

    public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
        ContextScope(if (context[Job] != null) context else context + Job())

В следующем примере демонстрируется создание нового CorotuineScope без какого-либо задания, переданного извне, но созданного внутри. Это также показывает проблеск структурированного параллелизма. Дочерняя сопрограмма (C*Hilda*) создает исключения, которые приводят к отмене другой сопрограммы ( Дочерняя-B ).

область действия сопрограммы (блок)

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

    public suspend fun  coroutineScope(block: suspend CoroutineScope.() -> R): R

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

Глобальный охват

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

    public object GlobalScope : CoroutineScope {
        override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext
    }

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

Причина избегать глобального охвата. Мы не рекомендуем использовать глобальную область действия… |автор Роман Елизаров | Средний

Роман Елизаров ・ 28 января 2019 г. ・ 3 мин Среда чтения

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

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

Это просто карта между ключом и Элемент (Ключ -> Элемент) где

  • Ключ : Ключ для элементов контекста сопрограммы типа

  • Элемент : Подтип контекста Сопрограммы, например, Задание, Отложенный, Диспетчер сопрограммы, Сопрограмма Актера и т.д.

Постановка проблемы

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

Есть несколько способов добиться этого, но я покажу, как использовать однопоточный executorservice.

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

Работа & Структурированный параллелизм

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

Сопрограммы реализуют Задание интерфейс, который отвечает за поддержание жизненного цикла сопрограммы.

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

Допустим, у нас есть три работы A, B и C. Работа B и C являются детьми работы A. Теперь, если, например, задание C завершается неудачно с исключением, отличным от исключения CancellationException, родительское задание A получает уведомление, и A немедленно отправляет завершение другим дочерним в этом случае заданию B. Таким образом достигается структурированный параллелизм.

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

Отношение родитель-ребенок имеет следующий эффект:

  • Отмена родителя с [отмена] или его исключительное завершение (неудача) немедленно отменяет всех его детей.

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

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

  • Неперехваченное исключение в дочернем по умолчанию отменяет родительское. В частности, это относится к дочерним элементам, созданным с помощью [запуска] [Область действия сопрограммы.запуск] конструктора сопрограмм.

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

Отсроченный

Из документов: Отложенное значение – это неблокирующее отменяемое будущее; это Работа с результатом.

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

Строители сопрограмм

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

Два наиболее часто используемых конструктора сопрограмм – это запуск и асинхронность.

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

Запуск

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

Запуск используется для выполнения асинхронного запуска и забывания типа операций, когда вас не интересует результат операции.

    public fun CoroutineScope.launch(
      context: CoroutineContext = EmptyCoroutineContext,
      start: CoroutineStart = CoroutineStart.DEFAULT,
      block: suspend CoroutineScope.() -> Unit
    ): Job

Асинхронный

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

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

    public fun  CoroutineScope.async(
      context: CoroutineContext = EmptyCoroutineContext,
      start: CoroutineStart = CoroutineStart.DEFAULT,
      block: suspend CoroutineScope.() -> T
    ): Deferred

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

Каково поведение по умолчанию при запуске и асинхронности?

  • Выполнение сопрограммы начинается немедленно

  • Вы можете переопределить это поведение, передав другой аргумент Запуск сопрограммы при запуске сопрограммы, например запуск. ленивый

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

  • Вы можете переопределить это поведение, предоставив явный Работа супервайзера при создании Область применения сопрограммы но это выходит за рамки данной статьи

  • Сопрограммы выполняются По умолчанию Диспетчер сопрограмм . Это поддерживается общим пулом потоков, и максимальное количество потоков равно количеству ядер процессора (не менее двух). Например, если вы хотите выполнить какую-либо операцию ввода-вывода, вы можете переопределить это поведение, передав пользовательские Контекст сопрограммы как контекст. ио

С контекстом

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

с контекстом конструкция как раз предназначена для этой цели. Следующий пример демонстрирует использование этого:

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

Но если вы посмотрите на первые две строки вывода, обе были выполнены в одном потоке, но третья строка была выполнена в другом потоке. Это связано с тем, что диспетчер ввода-вывода совместно использует потоки с диспетчером по умолчанию, поэтому использование с контекстом (Диспетчеры. IO) не приводит к фактическому переключению на другой поток, и обычно выполнение продолжается в том же потоке. Это позволяет избежать затрат на переключение потоков. Но этот поток помечается как поток ввода-вывода и удаляется из пула потоков. Следовательно, вы видите, что третья строка была выполнена в другом потоке.

    ========= Output =======
    DefaultDispatcher-worker-1 [@coroutine#1] doing CPU work…
    DefaultDispatcher-worker-1 [@coroutine#1] doing IO work…
    DefaultDispatcher-worker-2 [@coroutine#1] back to doing CPU work…

блокировка запуска

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

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

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

  • блокировка запуска очень полезна в тестах , , вы можете обернуть свои тесты в блокировку запуска. Это гарантирует, что ваш тестовый код будет выполняться последовательно в одном потоке и не завершится до тех пор, пока не будут завершены все сопрограммы. Вам не нужно явно присоединяться к ним или ждать их. Ваши тесты похожи на тесты для синхронного кода. В следующем примере мы хотим протестировать функцию increment , которая увеличивает счетчик на 1 в асинхронном и неблокирующем стиле. Тест - должен иметь возможность увеличивать счетчик, вызывает увеличение функцию 50 раз и утверждает, что

Более подробную информацию об этом можно найти здесь .

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

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

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

Правила вызова приостанавливающих функций:

  • от других приостанавливающих функций

  • из сопрограммы (приостанавливающие функции наследуют контекст сопрограммы из сопрограммы, из которой она вызывается)

В конце концов, код Kotlin преобразуется в байт-код JVM, и в JVM нет понятия ключевого слова suspend. Под капотом компилятор kotlin преобразует функции приостановки в другую функцию без ключевого слова suspend, которое принимает дополнительный параметр типа Продолжение , который является не чем иным, как обратным вызовом.

В следующем примере показана функция приостановки kotlin и ее скомпилированная версия в JVM

    // kotlin
    suspend fun updateUserInfo(name: String, id: Long): User

    // JVM
    public final Object updateUserInfo(String name, long id, Continuation $completion)

Продолжение

Продолжение – это простой интерфейс, определенный в стандартной библиотеке kotlin, который имеет только один метод возобновить с (результатом)

    public interface Continuation {
        public val context: CoroutineContext
        public fun resumeWith(result: Result)
    }

Начиная с версии 1.3, Продолжение имеет только один метод resume With (результат: Результат), ранее у него было два метода resume (значение: T) и resumeWithException (исключение: Выбрасываемый) Результат моделирует либо успех со значением типа, либо сбой, за исключением типа Throwable

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

Если вам действительно интересно узнать, как все это работает за кулисами, я настоятельно рекомендую вам посмотреть выступление Романа Елизарова — Глубокое погружение в Сопрограммы.

В этом выступлении Роман Елизаров упоминает:

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

Рекомендации

Оригинал: “https://dev.to/_pritam_kadam_/everything-you-need-to-know-about-kotlin-coroutines-4bfg”