Моя повестка дня для этого поста в блоге состоит в том, чтобы ознакомиться с различными терминологиями сопрограмм и ответить на следующие вопросы:
Разница между Работой и Отложенный , запуск и асинхронный
Какой конструктор сопрограмм вы должны использовать?
Что происходит, когда в сопрограмме возникает исключение ?
На очень высоком уровне, как достигается структурированный параллелизм ?
Потокобезопасное общее изменяемое состояние с однопоточным диспетчером
Ввод-вывод и процессор связанные операции с сопрограммами
Чтобы понять, как работают сопрограммы, и эффективно использовать их в реальных приложениях, вам необходимо сначала понять их основные концепции.
В остальной части статьи мы рассмотрим следующие темы:
Строители сопрограммной области ((Основная область, Область сопрограммы, область сопрограммы, Глобальная область и т.д.)
Контекст сопрограммы (Работа, Отложенная и т.д.)
Построители сопрограмм (запуск, асинхронность, с контекстом, блокировка запуска и т.д.)
Структурированный параллелизм
Приостановить
Продолжение
Вступление
Сопрограммы являются легкими потоками, и конструкция сопрограммы очень дешевая. Они напрямую не сопоставляются с потоками собственной операционной системы, из-за этого они очень быстрее создаются и уничтожаются по сравнению с потоками. Нет никаких дополнительных затрат на переключение контекста между потоками. Практически у вас могут быть тысячи или даже десятки тысяч сопрограмм. Может быть только одна нить, имеющая тысячи сопрограмм.
Двумя наиболее важными строительными блоками для создания/запуска/запуска новых сопрограмм являются область применения сопрограммы и строители сопрограмм .
Область действия сопрограммы состоит из всего оборудования, необходимого для запуска сопрограммы, например, она знает, где (в каком потоке) запускать сопрограмму, и конструкторы сопрограмм используются для создания новой сопрограммы.
Если мне нужно провести аналогию с потоками, область действия сопрограммы можно рассматривать как 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 funcoroutineScope(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 funCoroutineScope.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”