¿Qué es el modelo de actores (al estilo Akka), y cómo esquiva los problemas de concurrencia de memoria compartida?
Los actores cambian el estado mutable compartido por estado privado y un mailbox — cada uno procesa exactamente un mensaje a la vez, así que las races dentro de un actor se vuelven estructuralmente imposibles.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué es el modelo de actores (al estilo Akka), y cómo esquiva los problemas de concurrencia de memoria compartida?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Concurrencia sin memoria compartida
La mayoría de los bugs de concurrencia —race conditions, deadlocks, bugs de visibilidad donde un hilo nunca ve la escritura de otro— se remontan a la misma causa raíz: varios hilos leyendo y escribiendo la misma memoria mutable, coordinados (o no) con locks. Los locks arreglan la correctitud a costa del throughput e introducen sus propios modos de falla: deadlock, livelock, inversión de prioridad, lock convoy. El modelo de actores, introducido por Carl Hewitt en 1973 y popularizado en sistemas de producción por Erlang/OTP y después por Akka en la JVM, esquiva todo el problema eliminando de entrada la memoria mutable compartida como opción.
#Las tres garantías que da un actor
- El estado interno de un actor es privado — nada fuera del actor puede leerlo o escribirlo directamente, ni siquiera otro actor que tenga una referencia a él.
- La única forma de interactuar con un actor es enviándole un mensaje asíncrono e inmutable, que aterriza en el mailbox de ese actor, una cola.
- Un actor procesa exactamente un mensaje a la vez, de principio a fin, antes de mirar el siguiente en su mailbox.
La garantía 3 es la que más importa para la concurrencia: como un actor es efectivamente de un solo hilo respecto a su propio estado, nunca hay una race entre dos mensajes manejados "al mismo tiempo" para el mismo actor. Obtenés gratis la seguridad de un modelo mental de un solo hilo, mientras seguís teniendo concurrencia real a nivel de todo el sistema, porque distintos actores procesan sus propios mailboxes en paralelo, sobre un pool compartido de hilos subyacentes que el sistema de actores administra por vos (Akka le llama a este componente dispatcher).
#Qué sacrificás a cambio
Sin estado compartido no hay almuerzo gratis en los invariantes entre actores. Si dos actores necesitan una vista conjunta consistente —por ejemplo, transferir plata entre dos actores de cuenta— no podés simplemente tomar un lock que abarque a ambos; modelás la coordinación explícitamente, con un protocolo de request/response, una conversación de varios pasos, o ruteando ambas operaciones a través de un único actor dueño (o un patrón saga / process-manager) que serializa la decisión. El manejo de fallas es igual de explícito y estructural en vez de defensivo: Akka propaga la falla de un actor que crasheó hacia arriba, a un supervisor, que decide si reiniciarlo, retomarlo, detenerlo, o escalar más arriba. Ésta es la filosofía "dejalo crashear" de Erlang — en vez de codear defensivamente cada posible camino de falla dentro de cada handler de mensaje, aislás la falla a un solo actor y dejás que un supervisor por encima decida cómo recuperarse.
#Código
Abajo, un AccountActor guarda su balance como un valor local inmutable que se pasa a través de llamadas recursivas a apply, una forma común e idiomática de modelar el estado de un actor de forma funcional en Akka Typed: cada handler de mensaje devuelve el Behavior que debe procesar el próximo mensaje, capturando el nuevo estado en vez de mutar un campo.
import akka.actor.typed.{ActorRef, Behavior}import akka.actor.typed.scaladsl.Behaviorsobject AccountActor {sealed trait Commandfinal case class Deposit(amount: BigDecimal) extends Commandfinal case class Withdraw(amount: BigDecimal, replyTo: ActorRef[WithdrawResult]) extends Commandfinal case class GetBalance(replyTo: ActorRef[BigDecimal]) extends Commandsealed trait WithdrawResultcase object Approved extends WithdrawResultcase object InsufficientFunds extends WithdrawResultdef apply(balance: BigDecimal = 0): Behavior[Command] =Behaviors.receiveMessage {case Deposit(amount) =>apply(balance + amount)case Withdraw(amount, replyTo) if amount <= balance =>replyTo ! Approvedapply(balance - amount)case Withdraw(_, replyTo) =>replyTo ! InsufficientFundsBehaviors.samecase GetBalance(replyTo) =>replyTo ! balanceBehaviors.same}}
Ningún campo de este actor se muta jamás y no aparece ningún lock, y sin embargo mil mensajes Deposit y Withdraw concurrentes enviados al mismo ActorRef[Command] se manejan igual con consistencia interna perfecta, porque el mailbox del actor los serializa de a uno antes de que apply vea el siguiente mensaje.