Kotlin Suspending Computations — Parte I
Uma visão de como as coisas funcionam por baixo dos panos.
O que veremos?
Quando o tema é concorrência em Kotlin envolvendo coroutines, nos deparamos o tempo inteiro com as suspending computations. Para que possamos nos aprofundar e escrever melhores soluções usando coroutines, é interessante entender como as coisas funcionam por baixo dos panos. Por isso, nessa sequência de artigos que está se iniciando agora, veremos como o compilador transforma suspending functions em state machine, como ocorre a mudança de threads, e como as exceptions são propagadas.
Continuation Passing Style (CPS)
A atual implementação de operações suspensas (suspend functions/lambdas) é feita usando Continuation Passing Style.
CPS é um paradigma no contexto de programação funcional, o qual tem como objetivo passar o controle da execução na forma de continuation
. Ou seja, toda função recebe um parâmetro a mais, um "continuation". Quando a função termina a sua execução, ela "retorna" chamando o continuation passando o resultado da execução como parâmetro.
Imagine continuations como callbacks.
Nunca uma suspending computation chama outra diretamente. Ao invés disso, a função irá chamar um continuation que será executado após o término da execução da função.
O compilador se encarrega de fazer todo o trabalho pesado de transformar tudo que você escreve com o modificador suspend
, em funções que enviam e recebem continuations. Por fim, as suspending computations são transformadas em máquinas de estado que podem salvar e restaurar o estado, e executar um trecho de código. Dessa forma é que se torna possível suspender a execução em um determinado momento, salvar o estado enquanto esperam por outras computations terminarem, e posteriormente restaurar o estado e retomar a execução onde tinha parado anteriormente.
Continuations
O alicerce das suspending computations são as continuations. Elas são tão importantes porque são elas que permitem que uma coroutine seja “pausada” e retomada em um momento posterior.
Uma continuation é uma representação do fluxo de controle do seu programa em um “momento no tempo”. Ele permite que você “guarde” as instruções das suspending functions para posteriormente executá-las. Quando a continuation é chamada, o “estado” atual do programa é substituído pelo estado em que a continuation foi chamada.
As continuations permitem que você literalmente “pule” para diferentes partes no seu código. Elas são primitivas de baixo nível que fornecem controle sobre o fluxo de execução, permitindo implementar tudo de maneira que possa ser pausado ou retomado.
Como o assunto é um pouco longo e complexo, vou deixar a recomendação de um artigo que detalha bem o assunto: What’s in a Continuation.
Vamos dar uma olhada na interface Continuation da stdlib do Kotlin.
Podemos ver que a interface é simples. Ela contém:
CoroutineContext
que indica o contexto atual da coroutine quando entrou no ponto de suspensão;resumeWith
, uma função que recebe o resultado da operação que causou a suspensão, podendo ser umResult.success
ouResult.failure
.
Ainda tem algumas extensions functions que são usadas para indicar sucesso ou erro na execução:
Ter a referência do contexto atual da coroutine é uma parte muito importante, pois permite que cada continuation seja executada em uma thread ou pool de threads específico, ou ainda utilizar diferentes exception handlers.
O modificador “suspend”
Um dos mais importantes objetivos do time do Kotlin era permitir que com poucas modificações fosse possível dar suporte à concorrência de forma elegante na linguagem. Por isso, o “peso” do uso de concorrência é distribuído entre o compilador, a lib standard do Kotlin (stdlib), e a lib de coroutines.
Do ponto de vista da linguagem, apenas é preciso adicionar o modificador suspend
e o compilador já vai saber que precisa usar continuations no escopo daquela função ou lambda. Dessa maneira, nunca uma suspending computation é compilada de fato e, ao invés disso, o bytecode resultante é um continuation.
Vamos ver um exemplo:
A função getUserProfile
é uma suspending computation e dentro dela chamamos mais duas outras funções que também são suspending, getUserByName
e getProfileByUserId
. A seguir, a assinatura das funções citadas:
Após o compilador ler essas funções, podemos observar que a assinatura é modificada e um novo parâmetro é inserido em cada função (como foi citado anteriormente).
Alguns detalhes de implementação após a compilação foram omitidos para focarmos apenas na observação do
Continuation
.
Também podemos perceber que o retorno das funções foi modificado e agora todas elas retornam um Object
ao invés dos seus respectivos retornos. Mas por quê isso acontece?
Como nenhuma suspending computation chama outra diretamente, elas retornam a referência do Continuation
, o qual saberá resolver as operações necessárias para obter o resultado do retorno da função.
Na parte II continuaremos nossa busca por desvendar tudo que acontece por baixo dos panos com as suspending computations, e vamos falar sobre como o compilador cria e usa máquina de estado para trabalhar com continuations.
Se esse artigo agregou alguma coisa em seu conhecimento, não deixe de compartilhar! Obrigado por ler até aqui!😁👍🏻
Referências: