Introdução às Kotlin Coroutines
*1. Visão geral *
Neste artigo, veremos as corotinas da linguagem Kotlin. Simplificando, as* corotinas nos permitem criar programas assíncronos de uma maneira muito fluente *, e são baseadas no conceito de _https://en.wikipedia.org/wiki/Continuation-passing_style [estilo de passagem de continuação] _ programação .
A linguagem Kotlin nos fornece construções básicas, mas pode obter acesso a corotinas mais úteis com a biblioteca kotlinx-coroutines-core. Veremos esta biblioteca quando entendermos os blocos de construção básicos da linguagem Kotlin.
*2. Criando uma Coroutine com BuildSequence *
Vamos criar uma primeira rotina usando a função _https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/build-sequence.html [buildSequence] _.
E vamos implementar um gerador de sequência Fibonacci usando esta função:
val fibonacciSeq = buildSequence {
var a = 0
var b = 1
yield(1)
while (true) {
yield(a + b)
val tmp = a + b
a = b
b = tmp
}
}
A assinatura de uma função yield é:
public abstract suspend fun yield(value: T)
A palavra-chave suspend significa que esta função pode estar bloqueando. Essa função pode suspender uma rotina de buildSequence.
*As funções de suspensão podem ser criadas como funções padrão do Kotlin, mas precisamos estar cientes de que só podemos chamá-las de dentro de uma rotina.* Caso contrário, obteremos um erro do compilador.
Se suspendermos a chamada dentro da _buildSequence, a chamada será transformada no estado dedicado na máquina de estado. Uma corotina pode ser passada e atribuída a uma variável como qualquer outra função.
Na rotina fibonacciSeq, temos dois pontos de suspensão. Primeiro, quando estamos chamando yield (1) _ e segundo quando estamos chamando _yield (a + b) .
Se essa função yield resultar em alguma chamada de bloqueio, o encadeamento atual não será bloqueado. Ele poderá executar outro código. Depois que a função suspensa termina sua execução, o encadeamento pode retomar a execução da rotina fibonacciSeq.
Podemos testar nosso código usando alguns elementos da sequência de Fibonacci:
val res = fibonacciSeq
.take(5)
.toList()
assertEquals(res, listOf(1, 1, 2, 3, 5))
*3. Adicionando a dependência do Maven para kotlinx-coroutines *
Vejamos a biblioteca kotlinx-coroutines que possui construções úteis construídas sobre as corotinas básicas.
Vamos adicionar a dependência à biblioteca kotlinx-coroutines-core. Observe que também precisamos adicionar o repositório jcenter:
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>0.16</version>
</dependency>
<repositories>
<repository>
<id>central</id>
<url>http://jcenter.bintray.com</url>
</repository>
</repositories>
===* 4. Programação assíncrona usando a rotina C____unch ()
A biblioteca kotlinx-coroutines adiciona muitas construções úteis que nos permitem criar programas assíncronos. Digamos que temos uma função de computação cara que está anexando um String à lista de entrada:
suspend fun expensiveComputation(res: MutableList<String>) {
delay(1000L)
res.add("word!")
}
Podemos usar uma _outura_corotina que executará essa função de suspensão de maneira não-bloqueadora - precisamos passar um pool de threads como argumento para ele.
A função launch está retornando uma instância Job na qual podemos chamar um método _join () _ para aguardar os resultados:
@Test
fun givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay() {
//given
val res = mutableListOf<String>()
//when
runBlocking<Unit> {
val promise = launch(CommonPool) {
expensiveComputation(res)
}
res.add("Hello,")
promise.join()
}
//then
assertEquals(res, listOf("Hello,", "word!"))
}
Para poder testar nosso código, passamos toda a lógica para a rotina runBlocking - que é uma chamada de bloqueio. Portanto, nosso _assertEquals () _ pode ser executado de forma síncrona após o código dentro do método _runBlocking () _.
Observe que, neste exemplo, embora o método launch () _ seja acionado primeiro, é um cálculo atrasado. O thread principal continuará anexando _ _ Hello, _ String à lista de resultados.
Após o atraso de um segundo introduzido na função expensiveComputation () _, a _ “palavra!” String será anexado ao resultado.
*5. As corotinas são muito leves *
Vamos imaginar uma situação em que queremos executar 100000 operações de forma assíncrona. Gerar um número tão alto de encadeamentos será muito caro e, possivelmente, produzirá uma OutOfMemoryException.
Felizmente, ao usar as corotinas, esse não é o caso. Podemos executar quantas operações de bloqueio quisermos. Sob o capô, essas operações serão tratadas por um número fixo de encadeamentos sem criação excessiva de encadeamentos:
@Test
fun givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory() {
runBlocking<Unit> {
//given
val counter = AtomicInteger(0)
val numberOfCoroutines = 100_000
//when
val jobs = List(numberOfCoroutines) {
launch(CommonPool) {
delay(1000L)
counter.incrementAndGet()
}
}
jobs.forEach { it.join() }
//then
assertEquals(counter.get(), numberOfCoroutines)
}
}
Observe que estamos executando 100.000 corotinas e cada execução adiciona um atraso substancial. No entanto, não há necessidade de criar muitos threads porque essas operações são executadas de maneira assíncrona usando o thread do CommonPool.
===* 6. Cancelamento e tempos limite *
*Às vezes, depois de acionar alguma computação assíncrona de longa duração, queremos cancelá-la porque não estamos mais interessados no resultado.*
Quando iniciamos nossa ação assíncrona com a rotina launch () _, podemos examinar o sinalizador _isActive. Esse sinalizador é definido como false sempre que o thread principal chama o método _cancel () _ na instância do _Job: _
@Test
fun givenCancellableJob_whenRequestForCancel_thenShouldQuit() {
runBlocking<Unit> {
//given
val job = launch(CommonPool) {
while (isActive) {
println("is working")
}
}
delay(1300L)
//when
job.cancel()
//then cancel successfully
}
}
Essa é uma maneira muito elegante e fácil de usar o mecanismo de cancelamento . Na ação assíncrona, precisamos apenas verificar se o sinalizador isActive é igual a false e cancelar nosso processamento.
Quando estamos solicitando algum processamento e não temos certeza de quanto tempo esse cálculo levará, é aconselhável definir o tempo limite dessa ação. Se o processamento não terminar dentro do tempo limite especificado, obteremos uma exceção e poderemos reagir adequadamente.
Por exemplo, podemos tentar novamente a ação:
@Test(expected = CancellationException::class)
fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut() {
runBlocking<Unit> {
withTimeout(1300L) {
repeat(1000) { i ->
println("Some expensive computation $i ...")
delay(500L)
}
}
}
}
Se não definirmos um tempo limite, é possível que nosso encadeamento seja bloqueado para sempre, porque esse cálculo será interrompido. Não podemos lidar com esse caso em nosso código se o tempo limite não estiver definido.
*7. Executando ações assíncronas simultaneamente *
Digamos que precisamos iniciar duas ações assíncronas simultaneamente e aguardar seus resultados posteriormente. Se nosso processamento demorar um segundo e precisarmos executá-lo duas vezes, o tempo de execução da execução do bloqueio síncrono será de dois segundos.
Seria melhor se pudéssemos executar ambas as ações em threads separados e aguardar esses resultados no thread principal.
*Podemos aproveitar a _async () _ coroutine para conseguir isso* iniciando o processamento em dois threads separados simultaneamente:
@Test
fun givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently() {
runBlocking<Unit> {
val delay = 1000L
val time = measureTimeMillis {
//given
val one = async(CommonPool) {
someExpensiveComputation(delay)
}
val two = async(CommonPool) {
someExpensiveComputation(delay)
}
//when
runBlocking {
one.await()
two.await()
}
}
//then
assertTrue(time < delay *2)
}
}
Após enviar os dois cálculos dispendiosos, suspendemos a corotina executando a chamada runBlocking () _. Depois que os resultados _one e two estiverem disponíveis, a corotina será retomada e os resultados serão retornados. A execução de duas tarefas dessa maneira deve levar cerca de um segundo.
Podemos passar CoroutineStart.LAZY como o segundo argumento para o método async () _, mas isso significa que o cálculo assíncrono não será iniciado até ser solicitado. Como estamos solicitando o cálculo na rotina _runBlocking, isso significa que a chamada para _two.await () _ será feita apenas quando o _one.await () _ for concluído:
@Test
fun givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently() {
runBlocking<Unit> {
val delay = 1000L
val time = measureTimeMillis {
//given
val one
= async(CommonPool, CoroutineStart.LAZY) {
someExpensiveComputation(delay)
}
val two
= async(CommonPool, CoroutineStart.LAZY) {
someExpensiveComputation(delay)
}
//when
runBlocking {
one.await()
two.await()
}
}
//then
assertTrue(time > delay* 2)
}
}
*A preguiça da execução neste exemplo em particular faz com que nosso código seja executado de forma síncrona.* Isso acontece porque quando chamamos _await () _, o thread principal é bloqueado e somente após a tarefa _one_ terminar a tarefa _two_ será acionada.
Precisamos estar cientes de executar ações assíncronas de uma maneira lenta, pois elas podem ser executadas de maneira bloqueadora.
8. Conclusão
Neste artigo, analisamos os conceitos básicos das corotinas Kotlin.
Vimos que buildSequence é o principal componente de todas as corotinas. Descrevemos a aparência do fluxo de execução nesse estilo de programação de passagem de continuação.
Finalmente, vimos a biblioteca kotlinx-coroutines que fornece muitas construções muito úteis para criar programas assíncronos.
A implementação de todos esses exemplos e trechos de código pode ser encontrada no GitHub project - este é um projeto do Maven, portanto, deve ser fácil importar e executar como está.