Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Android SDK
Code

Параллелизм и корутины в Котлине

Difficulty:IntermediateLength:MediumLanguages:

Russian (Pусский) translation by Anna k.Ivanova (you can also view the original English article)

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

Например, в Java 5 была представлена среда executor, которая позволяет вам отделить детали управления потоками от вашей бизнес-логики. Java 8 предлагает параллельные потоки, которые можно легко использовать с лямбда-выражениями. RxJava привносит реактивные расширения в Java, позволяя вам писать очень краткий и читаемый асинхронный код.

Kotlin поддерживает почти все эти подходы и предлагает несколько своих. В этом уроке я покажу вам, как вы можете использовать их в приложениях для Android.

Предпосылки

Чтобы следовать этому уроку, вам понадобится:

Если вам неудобно работать с лямбда-выражениями и интерфейсами SAM, я предлагаю вам также прочитать следующее руководство, прежде чем продолжить:

Вы также можете изучить все тонкости языка Kotlin в нашей серии Kotlin From Scratch.

1. Создание потоков

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

Вот как вы можете использовать функцию thread(), которая является частью стандартной библиотеки Kotlin, для быстрого создания и запуска нового потока:

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

Например, следующий код использует метод newFixedThreadPool() класса Executors для создания пула потоков, содержащего восемь многократно используемых потоков, и выполняет с ним большое количество фоновых операций:

На первый взгляд это может быть неочевидно, но в приведенном выше коде аргумент метода submit() службы executor на самом деле является объектом Runnable.

2. Получение результатов из потоков

Фоновые задачи, созданные с использованием интерфейса Runnable, не могут возвращать результаты напрямую. Если вы хотите получать результаты от своих потоков, вы должны использовать вместо этого интерфейс Callable, который также является интерфейсом SAM.

Когда вы передаете объект Callable методу submit() службы executor, вы получаете объект Future. Как следует из его названия, объект Future будет содержать результат Callable в какой-то момент в будущем, когда служба исполнителя закончит его запуск. Чтобы получить реальный результат от объекта Future, все, что вам нужно сделать, это вызвать его метод get(), но будьте осторожны, ваш поток заблокируется, если вы вызовете его преждевременно.

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

3. Синхронизация потоков

В отличие от Java, Kotlin не имеет ключевого слова synchronized. Следовательно, для синхронизации нескольких фоновых операций вы должны использовать аннотацию @Synchronized или встроенную функцию стандартной библиотеки synchronized(). Аннотация может синхронизировать весь метод, и функция работает с блоком операторов.

И аннотация @Synchronized, и функция synchronized() используют концепцию блокировки монитора.

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

В то время как аннотация @Synchronized блокирует монитор объекта, которому принадлежит связанный метод, функция synchronized() может заблокировать монитор любого объекта, переданного ему в качестве аргумента.

4. Понимание корутин

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

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

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

Чтобы иметь возможность использовать сопрограммы в своем проекте Android Studio, убедитесь, что вы добавили следующую compile зависимость в файл build.gradle модуля app:

5. Создание функций приостановки

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

Чтобы создать функцию приостановки, все, что вам нужно сделать, это добавить модификатор suspend к обычной функции. Вот типичная функция приостановки, выполняющая HTTP-запрос GET с использованием библиотеки khttp:

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

6. Создание сопрограмм

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

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

Возвращаемым значением функции launch() является объект Job, который можно использовать для управления сопрограммой. Например, вы можете вызвать его метод join(), чтобы дождаться завершения сопрограммы. Точно так же вы можете вызвать его метод cancel(), чтобы немедленно отменить сопрограмму.

Использование функции launch() очень похоже на создание нового потока с объектом Runnable, главным образом потому, что вы не можете вернуть из него никакого значения. Если вы хотите иметь возможность вернуть значение из вашей сопрограммы, вы должны создать его с помощью функции async().

Функция async() возвращает объект Deferred, который, как и объект Job, позволяет вам управлять сопрограммой. Однако это также позволяет вам использовать функцию await() для ожидания результата сопрограммы без блокировки текущего потока.

Например, рассмотрим следующие сопрограммы, которые используют функцию приостановки fetchWebsiteContents() и возвращают длины содержимого двух разных адресов веб-страниц:

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

Если вы теперь хотите использовать возвращенные длины, вы должны вызвать метод await() для обоих объектов Deferred. Однако, поскольку метод await() тоже является функцией приостановки, вы должны убедиться, что вызываете его из другой сопрограммы.

В следующем коде показано, как вычислить сумму двух длин, используя новую сопрограмму, созданную с помощью функции launch():

7. Использование сопрограмм в потоке пользовательского интерфейса

Сопрограммы используют внутренние фоновые потоки, поэтому по умолчанию они не запускаются в потоке пользовательского интерфейса приложения Android. Следовательно, если вы попытаетесь изменить содержимое пользовательского интерфейса вашего приложения из сопрограммы, вы столкнетесь с ошибкой во время выполнения. К счастью, запустить сопрограмму в потоке пользовательского интерфейса довольно просто: вам просто нужно передать объект UI в качестве аргумента вашему билдеру сопрограмм.

Например, вот как вы можете переписать последнюю сопрограмму для отображения суммы внутри виджета TextView:

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

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

Например, с помощью функции suspend delay(), которая является неблокирующим эквивалентом метода Thread.sleep(), теперь вы можете создавать анимации с помощью циклов. Чтобы помочь вам начать, вот пример сопрограммы, который увеличивает координату x виджета TextView каждые 400 мс, создавая эффект, похожий на выделение:

Заключение

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

Чтобы узнать больше о сопрограммах, вы можете обратиться к официальной документации. И пока вы здесь, посмотрите некоторые другие наши посты о разработке Kotlin и Android!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.