Темы против сопрограмм в Котлине

Темы против сопрограмм в Котлине

1. Вступление

В этом кратком руководстве мы собираемся создавать и выполнять потоки в Kotlin.

Позже мы обсудим, как вообще этого избежать, в пользуKotlin Coroutines.

2. Создание тем

Создание потока в Kotlin аналогично созданию потока в Java.

Мы могли бы расширить классThread (хотя это не рекомендуется, поскольку Kotlin не поддерживает множественное наследование):

class SimpleThread: Thread() {
    public override fun run() {
        println("${Thread.currentThread()} has run.")
    }
}

Или мы можем реализовать интерфейсRunnable:

class SimpleRunnable: Runnable {
    public override fun run() {
        println("${Thread.currentThread()} has run.")
    }
}

И так же, как в Java, мы можем выполнить его, вызвав методstart():

val thread = SimpleThread()
thread.start()

val threadWithRunnable = Thread(SimpleRunnable())
threadWithRunnable.start()

В качестве альтернативы, как и Java 8, Kotlin поддерживаетSAM Conversions, поэтому мы можем воспользоваться этим и передать лямбду:

val thread = Thread {
    println("${Thread.currentThread()} has run.")
}
thread.start()

2.2. Kotlinthread() Функция

Другой способ - рассмотреть функциюthread(), которую предоставляет Котлин:

fun thread(
  start: Boolean = true,
  isDaemon: Boolean = false,
  contextClassLoader: ClassLoader? = null,
  name: String? = null,
  priority: Int = -1,
  block: () -> Unit
): Thread

С помощью этой функции поток может быть создан и выполнен просто:

thread(start = true) {
    println("${Thread.currentThread()} has run.")
}

Функция принимает пять параметров:

  • start –  Для немедленного запуска потока

  • isDaemon - Чтобы создать поток как поток демона

  • contextClassLoader - загрузчик классов, используемый для загрузки классов и ресурсов

  • name – Чтобы установить имя потока

  • priority - установить приоритет потока

3. Котлинские сопрограммы

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

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

Чтобы преодолеть эти проблемы,Kotlin introduced a new way of writing asynchronous, non-blocking code; the Coroutine.

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

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

Прежде чем представлять конструкторы сопрограмм, которые Kotlin предоставляет «из коробки», мы должны обсудить контекст сопрограмм.

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

Основными элементами являются:

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

  • Dispatcher - определяет, какой поток или потоки использует соответствующая сопрограмма для своего выполнения. С диспетчеромwe can a confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined

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

3.2. launchс

launch function is a coroutine builder which starts a new coroutine without blocking the current thread и возвращает ссылку на сопрограмму как объектJob:

runBlocking {
    val job = launch(Dispatchers.Default) {
        println("${Thread.currentThread()} has run.")
    }
}

У него есть два необязательных параметра:

  • context - контекст, в котором выполняется сопрограмма, если не определена, она наследует контекст отCoroutineScope, из которого она запускается.

  • start - параметры запуска сопрограммы. По умолчанию сопрограмма немедленно запланирована для выполнения

Обратите внимание, что приведенный выше код выполняется в общем фоновом пуле потоков, потому что мы использовалиDispatchers.Default w, который запускает его вGlobalScope.

В качестве альтернативы мы можем использоватьGlobalScope.launch w, который использует тот же диспетчер:

val job = GlobalScope.launch {
    println("${Thread.currentThread()} has run.")
}

Когда мы используемDispatchers.Default илиGlobalScope.launch, мы создаем сопрограмму верхнего уровня. Несмотря на то, что он легкий, он все же потребляет некоторые ресурсы памяти во время работы.

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

runBlocking {
    val job = launch {
        println("${Thread.currentThread()} has run.")
    }
}

В этом случае мы запускаем новую сопрограмму внутри построителя скорутинrunBlocking (который мы опишем позже) без указания контекста. Таким образом, сопрограмма унаследует контекстrunBlocking.

3.3. asyncс

Еще одна функция, которую Kotlin предоставляет для создания сопрограммы, -async.

Функцияasync создает новую сопрограмму и возвращает будущий результат как экземплярDeferred<T>:

val deferred = async {
    [email protected] "${Thread.currentThread()} has run."
}

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

Как иlaunch,, мы можем указать контекст, в котором будет выполняться сопрограмма, а также параметр запуска:

val deferred = async(Dispatchers.Unconfined, CoroutineStart.LAZY) {
    println("${Thread.currentThread()} has run.")
}

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

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

In addition, Kotlin provides Dispatchers.IO that uses a shared pool of on-demand created threads:

val deferred = async(Dispatchers.IO) {
    println("${Thread.currentThread()} has run.")
}

Dispatchers.IO рекомендуется, когда нам нужно выполнять интенсивные операции ввода-вывода.

3.4. runBlockingс

Ранее мы рассматривалиrunBlocking, но теперь давайте поговорим о нем более подробно.

runBlocking - это функция, которая запускает новую сопрограмму и блокирует текущий поток до его завершения.

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

Чтобы дождаться результата, мы должны вызвать метод приостановкиawait():

// async code goes here

runBlocking {
    val result = deferred.await()
    println(result)
}

await()  - это то, что называется функцией приостановки. Suspend functions are only allowed to be called from a coroutine or another suspend function. По этой причине мы заключили его в вызовrunBlocking.

Мы используемrunBlocking в функцияхmain и в тестах, чтобы мы могли связать код блокировки с другим кодом, написанным в стиле приостановки.

Подобно тому, как мы это делали в других сборщиках сопрограмм, мы можем установить контекст выполнения:

runBlocking(newSingleThreadContext("dedicatedThread")) {
    val result = deferred.await()
    println(result)
}

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

4. Заключение

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

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

Как обычно, все примеры кода, показанные в этом руководстве, доступныover on Github.