Tutorial de plano de fundo do Android. Parte 5: Corotinas em Kotlin


Ilha Kotlin

Textos anteriores desta série: sobre AsyncTask , sobre Loaders , sobre Executors e EventBus , sobre RxJava .

Então chegou esta hora. Este é o artigo para o qual toda a série foi escrita: uma explicação de como a nova abordagem funciona "sob o capô". Se você ainda não sabe como usá-lo, aqui estão alguns links úteis para você começar:


E, ao dominar as corotinas, você pode se perguntar o que permitiu à Kotlin oferecer essa oportunidade e como ela funciona. Observe que aqui vamos nos concentrar apenas no estágio de compilação: você pode escrever um artigo separado sobre execução.

A primeira coisa que precisamos entender é que, no corpus, na verdade não existem corotinas. O compilador transforma a função com o modificador de suspensão em uma função com o parâmetro Continuation . Essa interface possui dois métodos:

abstract fun resume(value: T) abstract fun resumeWithException(exception: Throwable) 

Tipo T é o tipo de retorno da sua função de suspensão original. E aqui está o que realmente acontece: essa função é executada em um determinado segmento (paciência, também chegamos a isso), e o resultado é passado para a função resume dessa continuação, no contexto em que a função de suspensão foi chamada. Se a função não receber o resultado e gerar uma exceção, o resumeWithException será lançado, gerando um erro no código de chamada.

Ok, mas de onde veio a continuação? Claro, do construtor Corutin! Vejamos o código que cria qualquer corotina, por exemplo, o lançamento:

 public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } 

Aqui, o construtor cria uma corotina - uma instância da classe AbstractCoroutine, que, por sua vez, implementa a interface de Continuação. O método start pertence à interface Job. Mas encontrar a definição do método de início é muito difícil. Mas podemos vir aqui do outro lado. Um leitor atento já percebeu que o primeiro argumento para a função de inicialização é o CoroutineContext, e está definido como DefaultDispatcher por padrão. Despachantes são classes que controlam a execução de corotinas, portanto são definitivamente importantes para entender o que está acontecendo. Vejamos a declaração DefaultDispatcher:

 public actual val DefaultDispatcher: CoroutineDispatcher = CommonPool 

Então, na verdade, esse é o CommonPool, embora as docas java nos digam que isso pode mudar. O que é o CommonPool?

Este é um gerenciador de rotinas que usa o ForkJoinPool como uma implementação do ExecutorService. Sim, é: no final, todas as suas rotinas lambda são apenas Runnable, que entraram no Executor com um conjunto de transformações complicadas. Mas o diabo, como sempre, está nos detalhes.


Garfo? Ou participar?

A julgar pelos resultados da pesquisa no meu twitter, aqui preciso explicar brevemente o que é FJP :)


Primeiro, o ForkJoinPool é um executor moderno criado para uso com fluxos paralelos do Java 8. A tarefa original era um paralelismo eficiente ao trabalhar com a API Stream, que basicamente significa dividir os fluxos para processar parte dos dados e combiná-los quando todos os dados forem processados. Para simplificar, imagine que você tenha o seguinte código:

 IntStream .range(1, 1_000_000) .parallel() .sum() 

A quantidade desse fluxo não será calculada em um fluxo. Em vez disso, o ForkJoinPool dividirá recursivamente o intervalo em partes (primeiro em duas partes de 500.000, depois cada uma em 250.000 e assim por diante), calcule a soma de cada parte e combine os resultados em um único quantidade. Aqui está uma visualização desse processo:


Os encadeamentos são divididos para tarefas diferentes e mesclados novamente após a conclusão

A eficácia do FJP é baseada no algoritmo de "roubo de trabalho": quando um encadeamento específico fica sem tarefas, ele vai para as filas de outros encadeamentos de pool e rouba suas tarefas. Para uma melhor compreensão, você pode ver o relatório de Alexei Shipilev ou assistir a uma apresentação .

Bem, nós percebemos o que nossas corotinas estão fazendo! Mas como eles acabam lá?

Isso acontece dentro do método de despacho CommonPool #:

 _pool.execute(timeSource.trackTask(block)) 

O método de despacho é chamado no método resume (Value: T) em DispatchedContinuation. Parece familiar! Lembramos que a continuação é uma interface implementada no AbstractCoroutine. Mas como eles estão relacionados?

O truque está dentro da classe CoroutineDispatcher. Ele implementa a interface ContinuationInterceptor da seguinte maneira:

 public actual override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = DispatchedContinuation(this, continuation) 

Está vendo? Você fornece um bloco simples para o construtor corutin. Você não precisa implementar nenhuma interface sobre a qual deseja saber nada. A biblioteca da corotina cuida de tudo isso. Ela é
intercepta a execução, substitui a continuação por DispatchedContinuation e a envia ao executor, o que garante a execução mais eficiente do seu código.

Agora, a única coisa com a qual precisamos lidar é como o despacho é chamado a partir do método start. Vamos preencher essa lacuna. O método resume é chamado de startCoroutine na função de extensão do bloco:

 public fun <R, T> (suspend R.() -> T).startCoroutine( receiver: R, completion: Continuation<T> ) { createCoroutineUnchecked(receiver, completion).resume(Unit) } 

E startCoroutine, por sua vez, é chamado pelo operador "()" na enumeração CoroutineStart. Seu construtor aceita como o segundo parâmetro e o padrão é CoroutineStart.DEFAULT. Isso é tudo!

Essa é a razão pela qual admiro a abordagem corutin: não é apenas uma sintaxe espetacular, mas também uma implementação brilhante.

E para aqueles que leram até o final, eles são exclusivos: um vídeo do meu relatório “Não é necessário um violinista: recusamos o RxJava em favor da corotina em Kotlin” da conferência Mobius . Enjoy :)

Source: https://habr.com/ru/post/pt415335/


All Articles