O que e Actor em Kotlin?

O Actor e um padrao de concorrencia onde uma entidade isolada (o ator) recebe mensagens atraves de um canal, processa uma de cada vez e mantem seu proprio estado interno de forma segura. Em Kotlin, esse padrao e implementado com coroutines e channels, aproveitando a infraestrutura do kotlinx.coroutines.

A ideia central e simples: em vez de compartilhar estado entre varias threads e proteger com locks, voce envia mensagens para um ator que e o unico dono daquele estado. Como ele processa uma mensagem por vez, nao ha condicoes de corrida.

Por que usar Actors?

Quando multiplas coroutines precisam acessar e modificar um mesmo recurso, o jeito classico e usar Mutex ou synchronized. Funciona, mas pode ficar complexo e propenso a erros. O padrao Actor oferece uma alternativa elegante: encapsula o estado e a logica de mutacao em um unico ponto, comunicando-se exclusivamente por mensagens.

Imagine um contador que varias partes do sistema precisam incrementar. Com o padrao Actor, todas enviam uma mensagem de “incrementar” e o ator faz a atualizacao de forma sequencial e segura.

Sintaxe e uso basico

Em Kotlin, voce pode implementar um actor usando uma coroutine que le de um Channel. A biblioteca kotlinx.coroutines oferecia uma funcao actor (agora marcada como obsoleta na API experimental), mas o padrao continua valido e pode ser implementado manualmente.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

// Definindo os tipos de mensagem
sealed class MensagemContador
object Incrementar : MensagemContador()
object Decrementar : MensagemContador()
class ObterValor(val resposta: CompletableDeferred<Int>) : MensagemContador()

fun CoroutineScope.contadorActor() = actor<MensagemContador> {
    var contador = 0
    for (msg in channel) {
        when (msg) {
            is Incrementar -> contador++
            is Decrementar -> contador--
            is ObterValor -> msg.resposta.complete(contador)
        }
    }
}

Nesse exemplo, o ator e uma coroutine que fica em loop lendo mensagens do seu canal interno. Cada mensagem e processada sequencialmente, garantindo que o estado contador nunca sera acessado por duas coroutines ao mesmo tempo.

Exemplo completo com multiplas coroutines

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

sealed class MensagemContador
object Incrementar : MensagemContador()
class ObterValor(val resposta: CompletableDeferred<Int>) : MensagemContador()

fun CoroutineScope.contadorActor() = actor<MensagemContador> {
    var contador = 0
    for (msg in channel) {
        when (msg) {
            is Incrementar -> contador++
            is ObterValor -> msg.resposta.complete(contador)
        }
    }
}

fun main() = runBlocking {
    val ator = contadorActor()

    // Lancar 1000 coroutines que incrementam o contador
    val jobs = List(1000) {
        launch {
            repeat(100) {
                ator.send(Incrementar)
            }
        }
    }

    jobs.forEach { it.join() }

    // Obter o valor final
    val resposta = CompletableDeferred<Int>()
    ator.send(ObterValor(resposta))
    println("Contador final: ${resposta.await()}") // 100000

    ator.close()
}

Aqui, 1000 coroutines enviam 100 incrementos cada. O resultado sera sempre 100000, sem nenhuma condicao de corrida, porque todas as operacoes passam pelo canal do ator e sao processadas uma por uma.

Implementacao manual sem a funcao actor

Como a funcao actor da biblioteca esta marcada como @ObsoleteCoroutinesApi, voce pode implementar o mesmo padrao manualmente:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

class ContadorActor(scope: CoroutineScope) {
    private val canal = Channel<MensagemContador>(Channel.UNLIMITED)

    init {
        scope.launch {
            var contador = 0
            for (msg in canal) {
                when (msg) {
                    is Incrementar -> contador++
                    is Decrementar -> contador--
                    is ObterValor -> msg.resposta.complete(contador)
                }
            }
        }
    }

    suspend fun incrementar() = canal.send(Incrementar)
    suspend fun decrementar() = canal.send(Decrementar)

    suspend fun obterValor(): Int {
        val resposta = CompletableDeferred<Int>()
        canal.send(ObterValor(resposta))
        return resposta.await()
    }

    fun fechar() = canal.close()
}

Essa versao encapsula o canal e expoe metodos com nomes claros, tornando o uso mais idiomatico e seguro.

Quando usar Actors?

O padrao Actor e ideal nos seguintes cenarios:

  • Estado mutavel compartilhado: quando varias coroutines precisam ler e escrever o mesmo recurso. Em vez de usar Mutex, voce delega toda a mutacao ao ator.
  • Processamento sequencial de eventos: quando eventos chegam de fontes diferentes mas precisam ser tratados em ordem, como em filas de tarefas.
  • Isolamento de estado: quando voce quer garantir que apenas uma parte do codigo pode modificar determinado estado, facilitando testes e depuracao.
  • Sistemas reativos: em arquiteturas orientadas a eventos, actors funcionam como componentes independentes que se comunicam por mensagens.

Nao use actors quando o estado nao e compartilhado ou quando a logica e simples o suficiente para um Mutex resolver sem complicacao.

Erros comuns

  1. Esquecer de fechar o ator: se voce nao chamar close() no canal, a coroutine do ator vai ficar suspensa indefinidamente esperando novas mensagens, causando vazamento de memoria.

  2. Usar canal com buffer ilimitado sem controle: Channel.UNLIMITED pode consumir muita memoria se as mensagens forem produzidas mais rapido do que consumidas. Considere usar Channel.BUFFERED ou um tamanho fixo.

  3. Capturar estado externo na coroutine do ator: o ator deve ser o unico dono do seu estado. Se voce compartilhar variaveis externas, perde toda a garantia de seguranca.

  4. Nao tratar excecoes dentro do ator: se uma mensagem causar uma excecao nao tratada, a coroutine do ator morre e todas as mensagens subsequentes serao perdidas. Sempre use try-catch dentro do loop.

  5. Confundir actor com Flow: Flow e para streams de dados reativos. Actor e para encapsular estado mutavel com acesso concorrente. Sao padroes complementares, nao substitutos.

Actor vs Mutex

Ambos resolvem o problema de acesso concorrente, mas de formas diferentes:

  • Mutex protege uma secao critica. Qualquer coroutine pode acessar o estado diretamente, desde que adquira o lock.
  • Actor encapsula o estado. Ninguem acessa diretamente; tudo passa por mensagens.

O Actor tende a ser mais seguro em sistemas complexos porque elimina a possibilidade de alguem esquecer de usar o lock. Porem, o Mutex e mais simples para casos triviais.

Termos relacionados

  • Channel: o mecanismo de comunicacao que os actors usam internamente para receber mensagens.
  • Coroutine: a unidade de execucao leve que implementa o ator.
  • Mutex: alternativa para protecao de estado compartilhado, baseada em locks.
  • Flow: stream reativo de dados, complementar ao padrao Actor.
  • Dispatcher: controla em qual thread a coroutine do ator vai executar.
  • Job: representa o ciclo de vida da coroutine que implementa o ator.

O padrao Actor e uma ferramenta poderosa no arsenal de concorrencia do Kotlin. Embora a funcao actor da biblioteca esteja marcada como obsoleta, o padrao em si continua sendo uma das formas mais seguras e organizadas de lidar com estado compartilhado em sistemas concorrentes.