Kotlin Suspending Computations — Parte I

Jeziel Lago
5 min readMar 3, 2020

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 um Result.success ou Result.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).

Bytecode decompilado das funções citadas.

Alguns detalhes de implementação após a compilação foram omitidos para focarmos apenas na observação doContinuation.

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.

--

--