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
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.Usar canal com buffer ilimitado sem controle:
Channel.UNLIMITEDpode consumir muita memoria se as mensagens forem produzidas mais rapido do que consumidas. Considere usarChannel.BUFFEREDou um tamanho fixo.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.
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-catchdentro do loop.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.