Básico de corotinas em Kotlin

lissatransborda

Lissa Ferreira

Posted on August 24, 2021

Básico de corotinas em Kotlin

Kotlinautas

Esse conteúdo é oferecido e distribuído pela comunidade Kotlinautas, uma comunidade brasileira que busca oferecer conteúdo gratuito sobre a linguagem Kotlin em um espaço plural.

capa Kotlinautas

O quê são corotinas?

Corotinas (ou Coroutines) são um bloco de código que rodam concorrentemente com o resto do código, isso significa que podemos rodar dois blocos de código ao mesmo tempo, podendo assim ao mesmo tempo ler quanto enviar para um servidor por exemplo. Vamos ver mais sobre corotinas na prática durante o artigo.

Materiais

Será necessário ter o IntelliJ instalado na máquina e um conhecimento básico sobre a linguagem Kotlin.

Criando um projeto com Corotinas

Abra seu IntelliJ no menu inicial e clique em New Project:

botão New Project no menu inicial do IntelliJ

Depois, selecione a opção Kotlin DSL build script, selecione também a opção Kotlin/JVM, e opicionalmente remova a primeira opção Java. Essa opção não vai mudar em nada, pois ela dá suporte do Gradle á linguagem Java, mas apenas iremos usar Kotlin.

Após isso, clique em Next e escreva o nome do projeto e a localização na sua máquina. Essas duas opção são completamente pessoais, caso não tenha nenhuma ideia, coloque algo como Corotinas apenas como identificação.

Agora, com o projeto aberto, vá ao aquivo build.gradle.kts e adicione a dependência implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"), com a seção dependencies ficando assim:

dependencies {
    implementation(kotlin("stdlib"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
}
Enter fullscreen mode Exit fullscreen mode

Agora, clique no elefante no canto superior direito para carregar as alterações no Gradle.

Elefante do Gradle no canto superior direito

Após isso, poderemos começar a programar. Você pode criar um arquivo em src/main/kotlin/ chamado main.kt para ser o arquivo principal da aplicação.

Mas com qualquer nome de arquivo, como você irá usar as corotinas, sempre se lembre de importar a biblioteca de corotinas no começo do arquivo:

import kotlinx.coroutines.*
Enter fullscreen mode Exit fullscreen mode

Primeira Corotina

Vamos criar o primeiro exemplo, vamos criar uma corotina que irá rodar paralelamente com o código principal, o código principal apenas irá mostrar um "Olá", enquanto o código da corotina irá esperar um segundo, e após isso, irá mostrar um "Mundo!". Podemos fazer isso da seguinte forma:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Mundo!")
    }
    println("Olá")
}
Enter fullscreen mode Exit fullscreen mode

Coloque esse código no seu IntelliJ e rode. O output esperado desse código é esse:

Olá
Mundo!
Enter fullscreen mode Exit fullscreen mode

Agora vamos explicar o quê esse código está fazendo:

  • RunBlocking é um bloco que irá armazenar todas as corotinas de uma parte do código, como se criasse um contexto diferente do normal da main. Todas as corotinas devem estar dentro de um bloco runBlocking;
  • launch irá iniciar uma corotina, que irá funcionar concorrentemente (ao mesmo tempo) e independente do resto do código, podemos inserir quantos blocos launch que quisermos dentro de um mesmo código;
  • delay é uma função que faz a corotina esperar por um tempo em milisegundos, e voltar com o processamento após esse tempo. Essa função recebe um número do tipo Long, que pode ser criado colocando um L no final de um número;

O runBlocking guarda um launch dentro, iniciando uma nova corotina, que a primeira instrução é o delay(1000L), fazendo que a corotina espere por um segundo (1000 milisegundos), enquanto isso o código principal continua, mandando um Olá na tela. E após um segundo da corotina rodando, a proxima e ultima instrução manda um Mundo! na tela.

Refatorando para uma função

Agora vamos transformar o conteúdo de dentro do bloco launch em uma função. Para isso, iremos precisar usar um suspend antes da função. (Função de suspensão)

Mas, o quê é esse suspend?

funções com suspend são funções que podem ser usadas normalmente dentro de corotinas, mas podem usar algumas funções especiais, como a função delay que como foi explicado mais cedo, serve para fazer a corotina esperar um tempo em milisegundos.

Com isso em mente, vamos criar a função:

suspend fun escreverMundo() {
    delay(1000L)
    println("Mundo!")
}
Enter fullscreen mode Exit fullscreen mode

E agora na main, vamos tirar tudo de dentro do bloco launch e rodar a função escreverMundo() dentro:

fun main() = runBlocking {
    launch { escreverMundo() }
    println("Olá")
}
Enter fullscreen mode Exit fullscreen mode

Pronto! Agora nosso código está mais organizado, diminuindo o código da função main.

Escopo de Corotinas

Podemos também criar um escopo onde iremos armazenar corotinas dentro. Esse escopo se chama coroutineScope. Esse bloco é muito parecido com o bloco runBlocking, mas tem uma diferença, enquanto o runBlocking bloqueia a thread em uso enquanto está esperando algo, o coroutineScope libera a thread para outros usos enquanto espera algo.

Como o coroutineScope consegue fazer isso?

Porque o coroutineScope é uma função de suspensão, enquanto o runBlocking é uma função normal. Por isso coroutineScope tem essas habilidades especiais.

Agora, vamos mudar a função escreverMundo, para fazer que essa função use os poderes de um coroutineScope:

suspend fun escreverMundo() = coroutineScope {
    launch {
        delay(1000L)
        println("Mundo!")
    }
    launch{
        delay(4000L)
        println("Já se passaram 4 segundos né?")
    }

    println("Olá")
}
Enter fullscreen mode Exit fullscreen mode
  • Agora, a função escreverMundo recebe uma coroutineScope;
  • Como uma coroutineScope, podemos colocar vários blocos launch dentro. No caso, há dois blocos;
  • O primeiro bloco, espera por um segundo e depois escreve um Mundo! na tela;
  • O segundo bloco espera por 4 segundos, e depois escreve na tela Já se passaram 4 segundos né?;
  • E abaixo destes dois blocos, há a instrução para escrever um Olá na tela.

Por conta que essas três partes serão executadas ao mesmo tempo, primeiro irá aparecer Olá, depois de um segundo Mundo!, e depois de 4 segundos que o programa começou a rodar, irá aparecer o Já se passaram 4 segundos né?.

Mas para que esse código rode corretamente, também precisamos mudar a função main adaptando para que possamos usar a função escreverMundo como coroutineScope

fun main() = runBlocking {
    escreverMundo()
}
Enter fullscreen mode Exit fullscreen mode

Agora, removemos o launch pois ele irá impedir que a main rode corretamente.

O resultado esperado do programa agora é:

Olá
Mundo!
Já se passaram 4 segundos né?
Enter fullscreen mode Exit fullscreen mode

Agora vamos fazer uma experiência, vamos remover o println("Olá") na função escreverMundo, e vamos colocar no final da função main, dessa maneira:

import kotlinx.coroutines.*

fun main() = runBlocking {
    escreverMundo()
    println("Olá")
}
suspend fun escreverMundo() = coroutineScope {
    launch {
        delay(1000L)
        println("Mundo!")
    }
    launch{
        delay(4000L)
        println("Já se passaram 4 segundos né?")
    }
}
Enter fullscreen mode Exit fullscreen mode

O resultado desse código é:

Mundo!
Já se passaram 4 segundos né?
Olá
Enter fullscreen mode Exit fullscreen mode
  • Como a função runBlocking bloqueia a thread enquanto está rodando, primeiro, todas as instruções de escreverMundo são rodadas, e após isso que o código irá continuar, mandando o Olá na tela.

Com todos esses recursos, dá pra fazer bastante coisa usando escopos de corotinas com coroutineScope, iniciar partes do código com corotinas com runBlocking, iniciar uma corotina com launch, e fazer uma corotina esperar um tempo com delay.

Jobs (Tarefas)

Jobs ou tarefas são instâncias de corotinas, que podem ser manipuladas para por exemplo, cancelar a corotina, esperar a corotina terminar todo o processamento para que o código principal continue,etc. Vamos ver esse exemplo abaixo:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val tarefa = launch {
        delay(1000L)
        println("Mundo!")
    }
    println("Olá")
    tarefa.join()
    println("Fim")
}
Enter fullscreen mode Exit fullscreen mode
  • main recebe um bloco runBlocking, podendo assim usar as corotinas dentro;
  • é criada uma variável chamada tarefa que recebe uma corotina em um bloco launch. Com isso, a corotina é iniciada e o código principal continua;
  • Após isso, é escrito na tela um Olá;
  • A função tarefa.join() faz com que a corotina tarefa tenha de terminar para que o código principal continue, com isso a instrução println("Fim") apenas irá rodar depois da corotina tarefa
  • Após isso, a corotina espera um segundo, com a instrução delay(1000L);
  • E ao final da corotina tarefa, é escrito um Mundo! na tela;
  • E depois da corotina tarefa ter acabado, é escrito um Fim na tela.

Com isso em mente, o output esperado é

Olá
Mundo!
Fim
Enter fullscreen mode Exit fullscreen mode

Mas, e se eu quiser que a corotina tarefa rode junto com o código da função main?

Podemos fazer isso mudando na linha 9 de tarefa.join() para tarefa.start(), com isso o nosso código ficará assim:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val tarefa = launch {
        delay(1000L)
        println("Mundo!")
    }
    println("Olá")
    tarefa.start()
    println("Fim")
}
Enter fullscreen mode Exit fullscreen mode

O output esperado dessa maneira é:

Olá
Fim
Mundo!
Enter fullscreen mode Exit fullscreen mode

Isso acontece pois enquanto a função tarefa.join() suspende a thread (main no caso) enquanto roda, a função tarefa.start() apenas inicia uma corotina (no caso a corotina tarefa), e continua a rodar o código principal.

Cancelando tarefas

Agora vamos aprender a como cancelar uma tarefa, esse conhecimento é útil para aplicações que irão rodar por muito tempo sem parar, e vão precisar iniciar e fechar corotinas constantemente, como por exemplo, uma aplicação web feita em Ktor. (Caso você tenha interesse em Ktor, leia esse artigo da Kotlinautas Criando uma API com Ktor)

Primeiro, vamos criar uma main que recebe um runBlocking:

import kotlinx.coroutines.*

fun main() = runBlocking{

}
Enter fullscreen mode Exit fullscreen mode

Agora, vamos criar uma variável tarefa que recebe um launch:

fun main() = runBlocking{
    val tarefa = launch {
        repeat(1000) { i ->
            println("tarefa: Estou rodando fazem $i vezes")
            delay(500L)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • A variável tarefa recebe um launch, logo sendo uma corotina;
  • Dentro da corotina, há um repeat(1000), esse repeat inicia um código que irá rodar por um número determinado de vezes, no caso, 1000 vezes;
  • E dentro desse bloco, é mostrado na tela um texto tarefa: Estou rodando fazem $i vezes, sendo $i o número de vezes que o repeat já repetiu;
  • Depois desse texto ser mostrado na tela, a corotina é suspensa por 500 milesegundos (meio segundo);

Agora, vamos fazer que a main espere um tempo, escreva na tela que não deseja mais esperar que a corotina tarefa termine seu processamento, cancele a corotina tarefa, e feche a main em seguida;

import kotlinx.coroutines.*

fun main() = runBlocking{
    val tarefa = launch {
        repeat(1000) { i ->
            println("tarefa: Estou rodando fazem $i vezes")
            delay(500L)
        }
    }
    delay(1300L)
    println("main: Não quero mais esperar pela tarefa!")
    tarefa.cancel()
    tarefa.join()
    println("main: Agora eu posso fechar")
}
Enter fullscreen mode Exit fullscreen mode
  • Agora, a main espera 1.3 segundos, e após isso, será mostrado na tela um texto main: Não quero mais esperar pela tarefa!;
  • Após isso, é usada a função tarefa.cancel() para cancelar a corotina, fazendo a corotina tarefa terminar;
  • Para fazer que o resto do código rode apenas quando a corotina for completamente cancelada, é usada a função tarefa.join() novamente;
  • Após isso, a main escreve na tela main: Agora eu posso fechar

O output esperado desse programa é:

tarefa: Estou rodando fazem 0 vezes
tarefa: Estou rodando fazem 1 vezes
tarefa: Estou rodando fazem 2 vezes
main: Não quero mais esperar pela tarefa!
main: Agora eu posso fechar
Enter fullscreen mode Exit fullscreen mode

Segundo a própria documentação do Kotlin, a função .cancel() cancela a tarefa (corotina sendo armazenada em uma variável), incluindo todas as corotinas iniciadas por essa.

Mas não é toda corotina que pode ser cancelada dessa maneira, vamos ver o exemplo á seguir:

import kotlinx.coroutines.*

fun main() = runBlocking{
    val tarefa = launch {
        while (isActive) {
            println("tarefa: Estou rodando!")
            delay(1000L)
        }
    }
    delay(5000L)
    println("main: Não quero mais esperar pela tarefa!")
    tarefa.cancel()
    tarefa.join()
    println("main: Agora eu posso fechar")
}
Enter fullscreen mode Exit fullscreen mode
  • Agora, ao invés de um repeat(1000), temos um while(isActive), isActive é uma variável interna da corotina, que sempre é verdadeira enquanto a corotina não terminou ou não foi cancelada. Logo, quando usamos tarefa.cancel(), a variável isActive se torna falsa e a corotina é cancelada.

O output esperado desse programa é:

tarefa: Estou rodando!
tarefa: Estou rodando!
tarefa: Estou rodando!
tarefa: Estou rodando!
tarefa: Estou rodando!
main: Não quero mais esperar pela tarefa!
main: Agora eu posso fechar
Enter fullscreen mode Exit fullscreen mode

Usando um try e finally dentro de uma corotina

Caso queiramos que a corotina faça algo antes de ser cancelada, podemos usar um bloco try com o código da corotina, e depois do try, dentro de um finally o código que irá rodar quando a corotina for cancelada.

Vamos usar o seguinte exemplo:

import kotlinx.coroutines.*

fun main() = runBlocking{
    val tarefa = launch {
        try {
                    var i = 0
                    while (isActive) {
                        println("tarefa: Estou rodando fazem $i vezes")
                        delay(1000L)
                        i++
                    }
                }finally {
                    println("tarefa: terminando corotina tarefa")
                }
    }
    delay(5000L)
    println("main: Não quero mais esperar pela tarefa!")
    tarefa.cancel()
    tarefa.join()
    println("main: Agora eu posso fechar")
}
Enter fullscreen mode Exit fullscreen mode
  • Agora, todo o código da corotina tarefa está dentro de um try, que é o mesmo código do exemplo anterior sobre isActive, mas agora, após o try, dentro de um finally, mostramos na tela tarefa: terminando corotina tarefa, mostrando esse conceito;

O output do programa é:

tarefa: Estou rodando fazem 0 vezes
tarefa: Estou rodando fazem 1 vezes
tarefa: Estou rodando fazem 2 vezes
tarefa: Estou rodando fazem 3 vezes
tarefa: Estou rodando fazem 4 vezes
main: Não quero mais esperar pela tarefa!
tarefa: terminando corotina tarefa
main: Agora eu posso fechar
Enter fullscreen mode Exit fullscreen mode

as linha 18 e 19 podem ser refatoradas em uma só, pois há o método cancelAndJoin(), que cancela a corotina e espera pelo seu fechamento. Com isso, o nosso código ficará assim:

import kotlinx.coroutines.*

fun main() = runBlocking{
    val tarefa = launch {
        try {
            var i = 0
            while (isActive) {
                println("tarefa: Estou rodando fazem $i vezes")
                delay(1000L)
                i++
            }
        }finally {
            println("tarefa: terminando corotina tarefa")
        }
    }
    delay(5000L)
    println("main: Não quero mais esperar pela tarefa!")
    tarefa.cancelAndJoin()
    println("main: Agora eu posso fechar")
}
Enter fullscreen mode Exit fullscreen mode

Timeout

É possível de criar corotinas com tempo máximo de existência, isso pode ser feito com withTimeout, informando um tempo do tipo Long, vamos supor o seguinte código:

import kotlinx.coroutines.*

fun main() = runBlocking{
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("Estou dormindo há $i ...")
            delay(500L)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Caso você tente rodar esse código, irá resultar neste erro:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
    at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
    at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
    at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
    at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
    at java.base/java.lang.Thread.run(Thread.java:829)

Process finished with exit code 1
Enter fullscreen mode Exit fullscreen mode

Nesse código, é usada a função withTimeout, que deixa fixo o tempo que uma corotina pode rodar. Caso esse tempo passe, é retornado um erro, sendo kotlinx.coroutines.TimeoutCancellationException.

Caso você queria que esse timeout não resulte em um erro, é possível se se usar a função withTimeoutOrNull, dessa maneira:

import kotlinx.coroutines.*

fun main() = runBlocking{
    val resultado = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("Estou dormindo $i ...")
            delay(500L)
        }
        "Feito"
    }
    println("Resultado é $resultado")
}
Enter fullscreen mode Exit fullscreen mode

Com isso, caso esse timeout resulte em um erro, a variável resultado receberá o valor null, mas caso deletemos a linha 7, que é uma espera na corotina que aumenta elevadamente o tempo de processamento, ultrapassando o valor determinado de 1.3 segundos pelo withTimeoutOrNull o valor de resultado será Feito pois a corotina rodou sem problema nenhum. Dessa maneira, o código ficará assim:

import kotlinx.coroutines.*

fun main() = runBlocking{
    val resultado = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("Estou dormindo $i ...")
        }
        "Feito"
    }
    println("Resultado é $resultado")
}
Enter fullscreen mode Exit fullscreen mode

Explorando mais sobre funções de suspensão

Vamos supor que temos duas funções, uma que retorna o número 10, e outra que retorna o número 20, e essas duas funções esperam por um segundo usando a função delay. Por conta dessas funções terem que pausar a sua execução, terão que ser funções de suspensão, tendo um suspend na frente. Dessa maneira:

suspend fun funçãoNúmeroUm(): Int {
    delay(1000L)
    return 10
}

suspend fun funçãoNúmeroDois(): Int {
    delay(1000L)
    return 20
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar uma main, que irá medir o tempo de execução total do código, criar duas variáveis, cada uma sendo o retorno dessas duas funções, e mostrar o resultado dessa maneira:

import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val tempo = measureTimeMillis {
        val um = funçãoNúmeroUm()
        val dois = funçãoNúmeroDois()
        println("A soma é ${um + dois}")
    }
    println("Feito em $tempo milisegundos")
}
Enter fullscreen mode Exit fullscreen mode
  • import kotlin.system.measureTimeMillis importa a função que irá medir o tempo do código;
  • O retorno das duas funções criadas anteriormente são armazenadas nas variáveis um e dois;
  • A soma dessas duas variáveis é mostrada na tela;
  • O tempo total dessas operações é guardado na variável tempo;
  • E o valor dessa variável tempo é mostrada na tela;

O output desse código será algo parecido com isso:

A soma é 30
Feito em 2008 milisegundos
Enter fullscreen mode Exit fullscreen mode

E se eu quiser rodar essas duas funções ao mesmo tempo, economizando tempo de processamento?

Isso pode ser feito usando a função async. A função async inicia uma corotina como a função launch, mas que pode receber um valor como retorno. Por isso é interessante usar async nesses casos, pois poderemos guardar o retorno de funções de suspensão dentro de variáveis.

Vamos ver como a nossa função main ficará com a função async:

fun main() = runBlocking {
    val tempo = measureTimeMillis {
        val um = async { funçãoNúmeroUm() }
        val dois = async { funçãoNúmeroDois() }
        println("A soma é ${um.await() + dois.await()}")
    }
    println("Feito em $tempo milisegundos")
}
Enter fullscreen mode Exit fullscreen mode
  • Agora, as funções funçãoNúmeroUm e funçãoNúmeroDois estão dentro de async, instânciando uma nova corotina (tarefa) para cada função;
  • Para pegar o valor de um e dois, é usada a função .await(), que pega o resultado de dentro da corotina;

Agora, o código roda na metade do tempo pois as duas funções estão rodando ao mesmo tempo:

A soma é 30
Feito em 1015 milisegundos
Enter fullscreen mode Exit fullscreen mode

Estruturando concorrências com async

Podemos melhorar ainda mais o código acima, estruturando essa concorrência em uma função, dessa maneira:

suspend fun soma(): Int = coroutineScope {
    val um = async { funçãoNúmeroUm() }
    val dois = async { funçãoNúmeroDois() }
    um.await() + dois.await()
}
Enter fullscreen mode Exit fullscreen mode
  • Criamos uma função soma que é um coroutineScope, esse escopo é muito interessante de ser usado nesse tipo de caso pois se uma corotina de dentro desse escopo falhar, todas as outras também irão falhar. No caso, as duas corotinas precisam dar um resultado válido para a função retornar o número coretamente.
  • E o retorno da função pega o valor das variáveis um e dois, e soma, retornando o resultado esperado de 30.

Agora também podemos mudar a função main para usar a função soma:

fun main() = runBlocking {
    val tempo = measureTimeMillis {
        println("A soma é ${soma()}")
    }
    println("Feito em $tempo milisegundos")
}
Enter fullscreen mode Exit fullscreen mode

Agora temos um código mais bem estruturado, seguro, e com seu output igual ainda:

A soma é 30
Feito em 1016 milisegundos
Enter fullscreen mode Exit fullscreen mode

E se alguma corotina der um erro, como posso tratar esse erro usando coroutineScope?

Vamos mudar a funçãoNúmeroDois para que essa função obrigatoriamente retorne um erro, dessa maneira:

suspend fun funçãoNúmeroDois(): Int {
    delay(1000L)
    return throw Exception("Função com erro esperado")
}
Enter fullscreen mode Exit fullscreen mode
  • Dessa maneira, obrigatoriamente, a funçãoNúmeroDois retorna um erro do tipo Função com erro esperado

Caso você tente rodar o código dessa maneira, dará um erro por conta da funçãoNúmeroDois:

Exception in thread "main" java.lang.Exception: Função com erro esperado
Enter fullscreen mode Exit fullscreen mode

Para resolver isso, pode ser usado com bloco try com um catch, dessa maneira, tratando o erro. Vamos mudar a função main mas tratando o erro:

fun main() = runBlocking {
    try {
        val tempo = measureTimeMillis {
            println("A soma é ${soma()}")
        }
        println("Feito em $tempo milisegundos")
    }catch(erro: Exception){
        println("Ocorreu um erro: $erro")
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora, o output do programa é:

Ocorreu um erro: java.lang.Exception: Função com erro esperado
Enter fullscreen mode Exit fullscreen mode

Mesmo que o erro Função com erro esperado tenha acontecido, a main fechou sem problemas, pois os blocos try e catch trataram o erro.

Finalização

Esse é o básico sobre corotinas no Kotlin. Há muito mais detalhes e conteúdos que podem ser abordados, mas para um artigo introdutório isso já é suficiente.

Muito obrigada por ler ❤️🏳️‍⚧️ e me segue nas redes, é tudo @lissatransborda 👀

💖 💪 🙅 🚩
lissatransborda
Lissa Ferreira

Posted on August 24, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related