Kotlin Coroutines — O que são?

Gabriel Kirsten
Coderef
Published in
6 min readJun 25, 2020

--

Nesse artigo vamos abordar um pouco dos problemas que as coroutines resolvem e como elas funcionam na linguagem Kotlin. Iremos apontar principalmente as diferenças entre Threads e Coroutines.

Esse artigo faz parte da nossa série sobre coroutines no Kotlin:

As coroutines são componentes dentro de um software que podem ter a sua execução suspensa e ter a sua atividade retomada em um momento futuro, possivelmente em outra Thread.

Hoje temos a tendência de aplicações e frameworks utilizando computação assíncrona, reativa ou não bloqueante e várias maneiras de resolver esse problema (como callbacks explicitos ou o RxJava), as coroutines são uma delas que a linguagem Kotlin oferece a nível de linguagem.

Logo do Kotlin

O conceito de coroutines não é exclusivo do Kotlin, algumas linguagens como Clojure e Rust implementam através de bibliotecas terceiras, outras linguagens como Kotlin e Go implementam de maneira nativa. Também encontramos coroutines expressas com um nome mais “artístico” e especifico de acordo da linguagem como: Goroutines (Go) e Cloroutine (Clojure).

No Kotlin, as coroutines estão disponíveis desde a versão 1.3 (Revision 3.3) e de maneira experimental na versão 1.1 e 1.2.

Diferença entre execução: bloqueante vs não bloqueantes e Threads vs Coroutines

Nessa sessão vamos abordar um pouco sobre os conceitos de bloqueante/não bloqueante e explicar um pouco a diferença entre Threads e Coroutines.

Para ilustrar esse problema, vamos considerar o seguinte cenário: Temos uma aplicação e será necessário executar um requisição via HTTP para um servidor remoto. Cada request HTTP conta com uma latência imposta pelo meio físico do trafego de dados e pelo processamento no servidor remoto. A imagem a seguir ilustra o cenário:

Duas aplicações trocando informações com o protocolo HTTP, a comunicação entre elas está sujeita a uma latência (destacada em azul), iremos analisar somente o fluxo que acontece na aplicação "Your App".

Solução síncrona e bloqueante (sem coroutines)

Vamos resolver o problema de maneira mais simples possível. Conforme a imagem a seguir, temos nossa Thread de processamento principal chamada de Main Thread, responsável por controlar o fluxo principal da aplicação. Quando enviamos a requisição na Main Thread vamos precisar aguardar a resposta do servidor remoto (trecho representado em vermelho), você pode hipoteticamente imaginar esse trecho de código como um simples loop infinito verificando se o servidor remoto respondeu. Enquanto o servidor não responder sua Thread fica bloqueada consumindo preciosos recursos de processamento.

Representação da execução de uma operação de consulta a um servidor HTTP, o trecho em vermelho representa o momento em que a Main Thread é bloqueada para esperar a resposta do servidor remoto.

Solução assíncrona e bloqueante (ainda sem coroutines)

Qual o problema do exemplo anterior? Basicamente consumo de recursos desnecessariamente alocados. Podemos encarar o exemplo como uma solução síncrona e bloqueante pois a execução do request foi realizada de forma que exista somente um fluxo de execução (somente uma Thread) e a Thread seja bloqueada executando um código que está somente esperando por algo.

Então como podemos resolver esse possível problema? Simplesmente executando o request HTTP em uma nova Thread, liberando a Thread principal para realizar outras ações, como renderizar uma interface de usuário por exemplo. Confira a imagem a seguir:

O fluxo de realização do request HTTP agora é executado em uma nova Thread (representado em azul), a Main Thread fica liberada (representado em verde) enquanto o request é realizado.

Solução Assíncrona e Não Bloqueante (com coroutines)

E agora? Qual o problema da nova solução? Por mais que estamos executando duas operações de maneira a liberar a Main Thread, ainda bloqueamos uma nova Thread que está provavelmente utilizando o poder de processamento de uma unidade de processamento enquanto aguardar o request.

Podemos então utilizar uma abordagem utilizando funções suspensas, onde podemos utilizar um client HTTP não bloqueante (Caso queira conhecer um exemplo, o WebClient do Spring implementa um cliente HTTP não bloqueante), o cliente HTTP não bloqueante pode utilizar uma suspend function do Kotlin para deixar a função suspensa até que seja recebida uma resposta do servidor HTTP, liberando a Thread para outro processamento até que seja chamado um callback para continuar a execução que dependa do response HTTP.

Veja o exemplo:

Solução de processamento HTTP através de uma abordagem não bloqueante.

Essa solução não ocupa processamento aguardando uma resposta HTTP e também poderia ser associada com outras Thread, mesclando um modelo de multi-threads com a abordagem de funções suspensas.

Ps. Nesse ponto é importante lembrar que coroutines não são sinônimos de paralelismo e sim de assíncronia, entretanto elas podem sim executar tarefas de maneira paralela dependendo da maneira em que o seu contexto for configurado, veremos em outro post sobre contexto de corroutines.

Comparações com as Threads

A propria documentação do Kotlin fala que você pode pensar em coroutines como uma light-weight thread, pois oferecem a possibilidade de ser executadas em paralelo, aguardar a sua execução e comunicar uma com as outras, porém é uma solução mais leve, podendo ser criada em centenas.

Várias coroutines podem ser criadas na mesma threads, em grandes quantidades. Tente executar um código que cria 100 mil coroutines, agora tente fazer o mesmo com Threads, provavelmente conhecerá (ou revisitará) o Out of Memory.

Diferença entre Thread e Coroutines na prática

Vamos implementar duas simples aplicações em Kotlin para exemplificar a diferença de uma Thread e uma Coroutine. A aplicação terá a simples tarefa de receber um número inteiro como parâmetro e retornar a sua soma com 10, essa função será chamada de addTen(), essa função vai ter um atraso de 1 segundo simulando uma operação demorada. Chamaremos a função duas vezes com os parametros 1 e 2 e vamos analisar o tempo de resposta de cada uma delas.

  • Solução utilizando Threads

O exemplo a seguir mostra então a aplicação utilizando Threads:

Ao analisar a resposta da aplicação, podemos ver o cenário que esperávamos. Para o primeiro caso (parâmetro 1) e para o segundo caso (parâmetro 2) temos as respostas 11 e 12, respectivamente, e o tempo de execução foi de 2 segundos, considerando 1 segundo para cada cálculo de addTen().

async task 1: 11
async task 2: 12
execution time: [2001] ms
  • Solução utilizando Coroutines

O Kotlin permite trabalhar com os famosos async/ await , bem conhecidos na comunidade de desenvolvedores Javascript ou C#. Veja o exemplo a seguir:

Podemos ver também a declaração da função addTen() o modificador suspend indicando que essa função pode ser suspensa, além de alterar o Thread.sleep()por uma chamada a função delay(), uma alternativa para pausar a execução de maneira não bloqueante utilizando coroutines. Outro ponto importante é que temos a utilização do runBlocking()na função main(), que executa uma nova coroutine e aguarda até a sua conclusão.

Vamos executar esse exemplo e analisar a resposta:

task 1: 11
task 2: 12
execution time: [1023] ms

Os resultados das operações são os mesmos, mas o tempo de execução foi cerca de 1 segundo, é praticamente a metade do tempo utilizado quando implementamos o exemplo utilizando Threads. Isso aconteceu pois a execução do delay()foi uma operação que não bloqueou a Thread, suspendendo a sua execução de maneira que fosse possível executar a próxima chamada da função addTen().

Lembrando novamente que como as coroutines, por padrão, não executam o código em paralelo, consequentemente, por mais que pareça a solução com coroutines não executou o código em paralelo mas suspendeu a execução pois a função delay() é uma operação não bloqueante que liberou a Thread. Tente executar o mesmo exemplo utilizando apenas alterando a chamada de delay() para Thread.sleep() e verá que mesmo a função addTen() sendo suspensa ela não conseguirá executar em metade do tempo pois a Thread não estará liberada.

What's Next

Neste post demos uma introdução importante sobre coroutines de maneira mais abstrata, no proximo post iremos abordar mais a fundo a sintaxe e como o Kotlin implementa esse padrão, a importante interface Continuation e o que acontece com uma suspend function depois de compilada.

Até mais

--

--