Introdução a corrotinas

Uma IU responsiva é um elemento essencial para um ótimo app. Embora você possa não ter tido problemas até agora nos apps que criou, quando começar a adicionar recursos mais avançados, como de rede ou de banco de dados, pode ser cada vez mais difícil escrever um código com bom funcionamento e desempenho. O exemplo abaixo ilustra apenas o que pode acontecer se tarefas longas, como download de imagens da Internet, não forem processadas corretamente. Embora a funcionalidade de imagem ainda funcione, a rolagem poderá sofrer cortes, fazendo com que a IU pareça não responder corretamente e não ser profissional.

9f8c54ba29f548cd.gif

Para evitar problemas com o app acima, você precisará aprender um pouco sobre algo chamado linhas de execução. Linha de execução é um conceito um pouco abstrato, mas você pode pensar nela como um caminho de execução de código no app. Cada linha de código que você escreve é uma instrução que precisa ser executada em ordem na mesma linha de execução.

Você já trabalhou com linhas de execução no Android. Todo app Android tem uma linha de execução "principal" padrão. Essa é (geralmente) a linha de execução de IU. Todo o código que você criou até agora fica na linha de execução principal. Cada instrução (ou seja, uma linha de código) aguarda a conclusão da anterior antes que a próxima linha seja executada.

No entanto, em um app em execução, há mais linhas de execução além da principal. Em segundo plano, o processador não funciona com linhas de execução separadas. Ele alterna entre as diversas séries de instruções para parecer ser multitarefas. Uma linha de execução é uma abstração que pode ser usada ao criar um código para determinar para qual caminho de execução cada instrução deve ir. Trabalhar com linhas de execução diferentes da principal permite que o app execute tarefas complexas em segundo plano, como o download de imagens, enquanto a interface do usuário do app continua responsiva. Isso é conhecido como código simultâneo, ou simplesmente simultaneidade.

Neste codelab, você aprenderá sobre linhas de execução e como usar o recurso de corrotinas do Kotlin para escrever códigos simultâneos claros e sem bloqueio.

Pré-requisitos

O que você aprenderá

  • O que é simultaneidade e por que ela é importante.
  • Como usar corrotinas e linhas de execução para criar código simultâneo sem bloqueio.
  • Como acessar a linha de execução principal para realizar atualizações de IU de forma segura ao realizar tarefas em segundo plano.
  • Como e quando usar um padrão de simultaneidade diferente (Scope/Dispatchers/Deferred).
  • Como criar código que interaja com recursos de rede.

O que você criará

  • Neste codelab, você criará alguns programas pequenos para explorar o trabalho com linhas de execução e corrotinas no Kotlin.

O que é necessário

  • Um computador com um navegador da Web moderno, como a versão mais recente do Chrome
  • Acesso à Internet no computador

Várias linhas de execução e simultaneidade

Até o momento, tratamos um app Android como um programa com apenas um caminho de execução. É possível usar esse único caminho de execução para muitas tarefas, mas conforme seu app cresce, você precisa pensar na simultaneidade.

A simultaneidade permite que várias unidades de código sejam executadas fora de ordem ou paralelamente, proporcionando um uso mais eficiente dos recursos. O sistema operacional pode usar as características do sistema, a linguagem de programação e a unidade de simultaneidade para gerenciar várias tarefas.

fe71122b40bdb5e3.png

Por que é preciso usar a simultaneidade? À medida que seu app fica mais complexo, é importante que o código não tenha bloqueios. Isso significa que a execução de uma tarefa de longa duração, como uma solicitação de rede, não interromperá a execução de outros elementos do app. Se a simultaneidade não for implementada corretamente, seu app poderá parecer não responder aos usuários.

Você verá vários exemplos que demonstram a programação simultânea em Kotlin. Todos os exemplos podem ser executados no Playground Kotlin:

https://developer.android.com/training/kotlinplayground

Uma linha de execução é a menor unidade de código que pode ser programada e executada em um programa. Veja um pequeno exemplo de onde podemos executar código simultâneo.

Você pode criar uma linha de execução simples fornecendo um lambda. Faça o seguinte no Playground.

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

A linha de execução não é executada até que a função alcance a chamada de função start(). O resultado será semelhante a este:

Thread[Thread-0,5,main] has run.

Observe que a currentThread() retorna uma instância da Thread convertida para a representação de string dela, que retorna o nome, a prioridade e o grupo de linhas de execução. A saída acima pode ser um pouco diferente.

Como criar e executar várias linhas de execução

Para demonstrar a simultaneidade simples, vamos criar algumas linhas de execução para serem executadas. O código cria três linhas de execução que imprimem a linha de informações no exemplo anterior.

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       Thread {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               Thread.sleep(50)
           }
       }.start()
   }
}

Saída no Playground:

Thread[Thread-2,5,main] has started Thread[Thread-2,5,main] - Starting Thread[Thread-0,5,main] - Doing Task 1 Thread[Thread-1,5,main] - Doing Task 1 Thread[Thread-2,5,main] - Doing Task 1 Thread[Thread-0,5,main] - Doing Task 2 Thread[Thread-1,5,main] - Doing Task 2 Thread[Thread-2,5,main] - Doing Task 2 Thread[Thread-0,5,main] - Ending Thread[Thread-2,5,main] - Ending Thread[Thread-1,5,main] - Ending Thread[Thread-0,5,main] has started
Thread[Thread-0,5,main] - Starting
Thread[Thread-1,5,main] has started
Thread[Thread-1,5,main] - Starting

Saída em AS (console):

Thread[Thread-0,5,main] has started
Thread[Thread-1,5,main] has started
Thread[Thread-2,5,main] has started
Thread[Thread-1,5,main] - Starting
Thread[Thread-0,5,main] - Starting
Thread[Thread-2,5,main] - Starting
Thread[Thread-1,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 1
Thread[Thread-2,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 2
Thread[Thread-1,5,main] - Doing Task 2
Thread[Thread-2,5,main] - Doing Task 2
Thread[Thread-0,5,main] - Ending
Thread[Thread-2,5,main] - Ending
Thread[Thread-1,5,main] - Ending

Execute o código várias vezes. Você verá resultados diferentes. Às vezes, as linhas de execução parecerão ser executadas em sequência, e em outros casos o conteúdo ficará intercalado.

Usar linhas de execução é um jeito simples de começar a trabalhar com várias tarefas e simultaneidade, mas ainda existem problemas. Vários deles podem surgir quando você usa a Thread diretamente no código.

As linhas de execução exigem muitos recursos.

A criação, a alternância e o gerenciamento de linhas de execução consomem recursos e tempo do sistema, limitando o número bruto de linhas de execução que podem ser gerenciadas ao mesmo tempo. Os custos de criação podem aumentar muito.

Enquanto um app em execução tiver várias linhas de execução, cada app terá uma linha de execução dedicada, responsável especificamente pela IU dele. Essa linha de execução muitas vezes é chamada de linha de execução principal ou linha de execução de IU.

Como essa linha de execução é responsável por executar a IU do app, é importante que a linha de execução principal tenha um bom desempenho para que o app funcione sem problemas. Qualquer tarefa de longa duração a bloqueará até ser concluída e fará com que seu app pare de responder.

O sistema operacional realiza várias tarefas para tentar manter tudo responsivo ao usuário. Os telefones atuais tentam atualizar a IU de 60 a 120 vezes por segundo (no mínimo 60). Há um curto tempo finito para preparar e desenhar a IU (a 60 quadros por segundo, toda atualização da tela precisa levar no máximo 16 ms). O Android eliminará quadros ou deixará de tentar concluir um ciclo de atualização para tentar manter a taxa de quadros. Alguns quadros são descartados e flutuações são normais, mas, se isso ocorrer muitas vezes, o app não responderá corretamente.

Disputas e comportamento imprevisível

Como discutido, uma linha de execução é uma abstração sobre como um processador parece gerenciar várias tarefas ao mesmo tempo. Como o processador alterna entre conjuntos de instruções em diferentes linhas de execução, o momento exato em que uma é executada ou pausada está além do seu controle. Você nem sempre pode esperar uma saída previsível ao trabalhar diretamente com linhas de execução.

Por exemplo, o código a seguir usa uma repetição simples para contar de 1 a 50, mas nesse caso uma nova linha de execução é criada cada vez que a contagem aumenta. Imagine a saída esperada e depois execute o código algumas vezes.

fun main() {
   var count = 0
   for (i in 1..50) {
       Thread {
           count += 1
           println("Thread: $i count: $count")
       }.start()
   }
}

A saída foi como você esperava? Foi sempre a mesma? Veja um exemplo de saída que tivemos.

Thread: 50 count: 49 Thread: 43 count: 50 Thread: 1 count: 1
Thread: 2 count: 2
Thread: 3 count: 3
Thread: 4 count: 4
Thread: 5 count: 5
Thread: 6 count: 6
Thread: 7 count: 7
Thread: 8 count: 8
Thread: 9 count: 9
Thread: 10 count: 10
Thread: 11 count: 11
Thread: 12 count: 12
Thread: 13 count: 13
Thread: 14 count: 14
Thread: 15 count: 15
Thread: 16 count: 16
Thread: 17 count: 17
Thread: 18 count: 18
Thread: 19 count: 19
Thread: 20 count: 20
Thread: 21 count: 21
Thread: 23 count: 22
Thread: 22 count: 23
Thread: 24 count: 24
Thread: 25 count: 25
Thread: 26 count: 26
Thread: 27 count: 27
Thread: 30 count: 28
Thread: 28 count: 29
Thread: 29 count: 41
Thread: 40 count: 41
Thread: 39 count: 41
Thread: 41 count: 41
Thread: 38 count: 41
Thread: 37 count: 41
Thread: 35 count: 41
Thread: 33 count: 41
Thread: 36 count: 41
Thread: 34 count: 41
Thread: 31 count: 41
Thread: 32 count: 41
Thread: 44 count: 42
Thread: 46 count: 43
Thread: 45 count: 44
Thread: 47 count: 45
Thread: 48 count: 46
Thread: 42 count: 47
Thread: 49 count: 48

Ao contrário do que diz o código, parece que a última linha de execução foi executada primeiro e que algumas das outras foram executadas fora de ordem. Se você observar a "contagem" de algumas das iterações, perceberá que ela permanece sem mudanças após várias linhas de execução. De forma ainda mais inesperada, a contagem alcança 50 na linha de execução 43, embora a saída sugira que essa seja apenas a segunda linha a ser executada. Com base na saída, é impossível saber qual é o valor final da count.

Essa é apenas uma maneira em que as linhas de execução podem gerar um comportamento imprevisível. Ao trabalhar com várias linhas de execução, você também pode encontrar o que é chamado de disputa. Isso ocorre quando várias linhas de execução tentam acessar o mesmo valor na memória ao mesmo tempo. Disputas podem resultar em bugs aleatórios e difíceis de reproduzir, que podem fazer com que seu app falhe, geralmente de forma imprevisível.

Problemas de desempenho, disputas e bugs difíceis de reproduzir são alguns dos motivos pelos quais não recomendamos trabalhar diretamente com linhas de execução. Em vez disso, você aprenderá sobre um recurso do Kotlin chamado corrotinas, que ajudará a criar código simultâneo.

O Android aceita a criação e o uso de linhas de execução para tarefas em segundo plano, mas o Kotlin também oferece as corrotinas, que proporcionam uma maneira mais flexível e fácil de gerenciar a simultaneidade.

As corrotinas permitem trabalhar com várias tarefas, mas fornecem outro nível de abstração do que simplesmente trabalhar com linhas de execução. Um dos principais recursos das corrotinas é a capacidade de armazenar o estado, para que ele possa ser interrompido e retomado. Uma corrotina pode ou não ser executada.

O estado, representado por continuações, permite que partes do código sinalizem quando precisam passar o controle ou esperar que outra corrotina termine o trabalho antes de ser retomada. Esse fluxo é chamado de multitarefa cooperativa. A implementação de corrotinas do Kotlin adiciona vários recursos para ajudar na realização de várias tarefas. Além das continuações, a criação de uma corrotina abrange esse trabalho em um Job, uma unidade de trabalho cancelável com um ciclo de vida, dentro de um CoroutineScope. O CoroutineScope é um contexto que aplica o cancelamento e outras regras nos próprios filhos e aos filhos deles de forma recorrente. Um Dispatcher gerencia qual linha de execução de apoio a corrotina usará para a execução, eliminando a responsabilidade do desenvolvedor por quando e onde usar uma nova linha de execução.

Job

Uma unidade de trabalho cancelável, como uma criada pela função launch().

CoroutineScope

Funções usadas para criar novas corrotinas, como launch() e async(), estendem o CoroutineScope.

Dispatcher

Determina a linha de execução que a corrotina usará. O dispatcher Main sempre executará corrotinas na linha de execução principal, enquanto os dispatchers Default, IO ou Unconfined usarão outras linhas de execução.

Você aprenderá mais sobre isso depois, mas os Dispatchers são uma das maneiras em que as corrotinas podem apresentar um desempenho tão bom. Eles evitam o custo de desempenho da inicialização de novas linhas de execução.

Vamos adaptar nossos exemplos anteriores ao uso de corrotinas.

import kotlinx.coroutines.*

fun main() {
    repeat(3) {
        GlobalScope.launch {
            println("Hi from ${Thread.currentThread()}")
        }
    }
}
Hi from Thread[DefaultDispatcher-worker-2@coroutine#2,5,main]
Hi from Thread[DefaultDispatcher-worker-1@coroutine#1,5,main]
Hi from Thread[DefaultDispatcher-worker-1@coroutine#3`,5,main]

O snippet acima cria três corrotinas no Global Scope usando o Dispatcher padrão. O GlobalScope permite que qualquer corrotina nele seja executada enquanto o app estiver em execução. Pelos motivos que discutimos anteriormente sobre a linha de execução principal, isso não é recomendado fora do código de exemplo. Ao usar corrotinas nos seus apps, use outros escopos.

A função launch() cria uma corrotina usando o código incluído em um objeto Job cancelável. A função launch() é usada quando um valor de retorno não é necessário fora dos limites da corrotina.

Veja a assinatura completa da função launch() para entender o próximo conceito importante em corrotinas.

fun CoroutineScope.launch {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
}

Em segundo plano, o bloco de código que você transmitiu para a inicialização é marcado com a palavra-chave suspend. Essa palavra-chave sinaliza que um bloco de código ou função pode ser pausado ou retomado.

Sobre o runBlocking

Os próximos exemplos usarão runBlocking(), que, como o nome indica, inicia uma nova corrotina e bloqueia a linha de execução atual até a conclusão. Essa função é usada principalmente para fazer a ponte entre códigos com e sem bloqueios em funções e testes principais. Você não a usará com frequência em códigos comuns do Android.

import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val formatter = DateTimeFormatter.ISO_LOCAL_TIME
val time = { formatter.format(LocalDateTime.now()) }

suspend fun getValue(): Double {
    println("entering getValue() at ${time()}")
    delay(3000)
    println("leaving getValue() at ${time()}")
    return Math.random()
}

fun main() {
    runBlocking {
        val num1 = getValue()
        val num2 = getValue()
        println("result of num1 + num2 is ${num1 + num2}")
    }
}

O getValue() retorna um número aleatório após um tempo de atraso definido. Ele usa um DateTimeFormatter para ilustrar os tempos de entrada e de saída adequados. A função principal chama o getValue() duas vezes e retorna a soma.

entering getValue() at 17:44:52.311
leaving getValue() at 17:44:55.319
entering getValue() at 17:44:55.32
leaving getValue() at 17:44:58.32
result of num1 + num2 is 1.4320332550421415

Para ver como isso funciona, substitua a função main() (mantendo o restante do código) pelo seguinte.

fun main() {
    runBlocking {
        val num1 = async { getValue() }
        val num2 = async { getValue() }
        println("result of num1 + num2 is ${num1.await() + num2.await()}")
    }
}

As duas chamadas para o getValue() são independentes e não precisam necessariamente que a corrotina seja suspensa. O Kotlin tem uma função assíncrona semelhante à de inicialização. A função async() é definida da seguinte forma.

Fun CoroutineScope.async() {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>

A função async() retorna um valor do tipo Deferred. Um Deferred é um Job cancelável que pode conter uma referência a um valor futuro. Ao usar um Deferred, você ainda pode chamar uma função como se ela retornasse um valor imediatamente. Um Deferred serve apenas como um marcador, já que não é possível saber quando uma tarefa assíncrona será retornada. Um Deferred (também chamado de Promise ou Future em outras linguagens de programação) garante que um valor será retornado mais tarde para esse objeto. Por outro lado, uma tarefa assíncrona não bloqueará ou aguardará a execução por padrão. Para indicar que a linha de código atual precisa aguardar a saída de um Deferred, você pode chamar await() nela. Essa função retornará o valor bruto.

entering getValue() at 22:52:25.025
entering getValue() at 22:52:25.03
leaving getValue() at 22:52:28.03
leaving getValue() at 22:52:28.032
result of num1 + num2 is 0.8416379026501276

Quando marcar funções como Suspend

No exemplo anterior, você pode ter percebido que a função getValue() também é definida com a palavra-chave suspend. O motivo é que ela chama a delay(), que também é uma função suspend. Sempre que uma função chama outra que seja suspend, ela também precisa ser suspend.

Se esse é o caso, por que a função main() no nosso exemplo não seria marcada com suspend? Afinal, ela chama o getValue().

Não necessariamente. Na verdade, o getValue() é chamado na função transmitida para a runBlocking(), que é uma função suspend semelhante às transmitidas para launch() e async(). No entanto, o getValue() não é chamado na main() e runBlocking() não é uma função suspend. Dessa forma, a main() não é marcada com suspend. Se uma função não chamar outra que seja suspend, ela não precisa ser suspend.

No início deste codelab, você viu o exemplo a seguir, que usava várias linhas de execução. Com seu conhecimento das corrotinas, reescreva o código para usar corrotinas em vez de Thread.

Observação: não é necessário editar as instruções println(), mesmo que elas referenciem a Thread.

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       Thread {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               Thread.sleep(50)
           }
       }.start()
   }
}
import kotlinx.coroutines.*

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       GlobalScope.launch {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               delay(5000)
           }
       }
   }
}

Você aprendeu

  • por que a simultaneidade é necessária;
  • o que é uma linha de execução e por que elas são importantes para a simultaneidade;
  • como escrever código simultâneo no Kotlin usando corrotinas;
  • quando marcar uma função como "suspend" ou não;
  • as funções do CoroutineScope, Job e Dispatcher;
  • a diferença entre Deferred e Await.