Threads vs Coroutines em Kotlin

Threads vs Coroutines em Kotlin

1. Introdução

Neste tutorial rápido, vamos criar e executar threads em Kotlin.

Mais tarde, discutiremos como evitá-lo completamente, em favor deKotlin Coroutines.

2. Criando Threads

Criar um thread em Kotlin é semelhante a fazer isso em Java.

Poderíamos estender a classeThread (embora não seja recomendado porque Kotlin não oferece suporte a herança múltipla):

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

Ou podemos implementar a interfaceRunnable:

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

E da mesma forma que fazemos em Java, podemos executá-lo chamando o métodostart():

val thread = SimpleThread()
thread.start()

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

Como alternativa, como o Java 8, o Kotlin oferece suporte aSAM Conversions, portanto, podemos aproveitá-lo e passar um lambda:

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

2.2. Função de Kotlinthread()

Outra maneira é considerar a funçãothread() que o Kotlin fornece:

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

Com esta função, um thread pode ser instanciado e executado simplesmente por:

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

A função aceita cinco parâmetros:

  • start – Para executar imediatamente o tópico

  • isDaemon - Para criar o thread como um thread daemon

  • contextClassLoader - Um carregador de classes para usar para carregar classes e recursos

  • name – Para definir o nome do tópico

  • priority - Para definir a prioridade da discussão

3. Corotinas Kotlin

É tentador pensar que gerar mais threads pode nos ajudar a executar mais tarefas simultaneamente. Infelizmente, isso nem sempre é verdade.

Criar muitos encadeamentos pode, na verdade, tornar um aplicativo com baixo desempenho em algumas situações; threads são objetos que impõem sobrecarga durante a alocação de objetos e a coleta de lixo.

Para superar esses problemas,Kotlin introduced a new way of writing asynchronous, non-blocking code; the Coroutine.

Semelhante aos encadeamentos, as corotinas podem executar simultaneamente, aguardar e se comunicar com a diferença de que criá-las é muito mais barata que os encadeamentos.

3.1. Contexto de co-rotina

Antes de apresentar os construtores de corotina que o Kotlin fornece imediatamente, precisamos discutir o contexto da corotina.

As corotinas sempre são executadas em algum contexto que é um conjunto de vários elementos.

Os principais elementos são:

  • Job - modela um fluxo de trabalho cancelável com vários estados e um ciclo de vida que culmina em sua conclusão

  • Dispatcher - determina qual thread ou threads a co-rotina correspondente usa para sua execução. Com o despachante,we can a confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined

Veremos como especificar o contexto enquanto descrevemos as corrotinas nas próximas etapas.

3.2. launch

Olaunch function is a coroutine builder which starts a new coroutine without blocking the current threade retorna uma referência à co-rotina como um objetoJob:

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

Possui dois parâmetros opcionais:

  • context - O contexto em que a co-rotina é executada, se não for definido, herda o contexto doCoroutineScope de onde está sendo iniciada

  • start - As opções de início para a co-rotina. Por padrão, a rotina é agendada imediatamente para execução

Observe que o código acima é executado em um pool de threads de fundo compartilhado porque usamosDispatchers.Default which o inicia noGlobalScope.

Como alternativa, podemos usarGlobalScope.launch which usa o mesmo despachante:

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

Quando usamosDispatchers.Default ouGlobalScope.launch, criamos uma co-rotina de nível superior. Mesmo que seja leve, ele ainda consome alguns recursos de memória enquanto é executado.

Em vez de lançar co-rotinas emGlobalScope, como costumamos fazer com threads (threads são sempre globais), podemos lançar corrotinas no escopo específico da operação que estamos executando:

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

Neste caso, começamos uma nova co-rotina dentro do construtor de scoroutinerunBlocking (que descreveremos mais tarde) sem especificar o contexto. Assim, a co-rotina herdará o contexto derunBlocking.

3.3. async

Outra função que o Kotlin fornece para criar uma co-rotina éasync.

A funçãoasync cria uma nova co-rotina e retorna um resultado futuro como uma instância deDeferred<T>:

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

deferred é um futuro cancelável sem bloqueio que descreve um objeto que atua como um proxy para um resultado que é inicialmente desconhecido.

Comolaunch,, podemos especificar um contexto no qual executar a co-rotina, bem como uma opção de início:

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

Neste caso, lançamos a co-rotina usandoDispatchers.Unconfined que inicia as co-rotinas no thread do chamador, mas apenas até o primeiro ponto de suspensão.

Observe queDispatchers.Unconfined é uma boa opção quando uma co-rotina não consome tempo de CPU nem atualiza nenhum dado compartilhado.

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 é recomendado quando precisamos fazer operações intensivas de E / S.

3.4. runBlocking

Vimos anteriormenterunBlocking, mas agora vamos falar sobre isso com mais detalhes.

runBlocking é uma função que executa uma nova co-rotina e bloqueia a thread atual até sua conclusão.

A título de exemplo, no snippet anterior, lançamos a corotina, mas nunca esperamos o resultado.

Para aguardar o resultado, temos que chamar o método de suspensãoawait():

// async code goes here

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

await()  é o que é chamado de função de suspensão. Suspend functions are only allowed to be called from a coroutine or another suspend function. Por esta razão, nós o incluímos em uma invocaçãorunBlocking.

UsamosrunBlocking em funçõesmain e em testes para que possamos vincular o código de bloqueio a outro escrito em estilo suspenso.

De maneira semelhante à de outros construtores de corotina, podemos definir o contexto de execução:

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

Observe que podemos criar um novo encadeamento no qual poderíamos executar a corotina. No entanto, um encadeamento dedicado é um recurso caro. E, quando não for mais necessário, devemos liberá-lo ou, melhor ainda, reutilizá-lo em todo o aplicativo.

4. Conclusão

Neste tutorial, aprendemos como executar código assíncrono e sem bloqueio, criando um encadeamento.

Como alternativa ao tópico, também vimos como a abordagem de Kotlin para usar corrotinas é simples e elegante.

Como de costume, todos os exemplos de código mostrados neste tutorial estão disponíveisover on Github.