La mayoría de nosotros, cuándo oímos hablar del modelo de actores (o actor model en inglés), o bien no sabemos de qué estamos hablando, o bien nos viene a la cabeza el framework Akka (para la JVM, entre cuyas funcionalidades destaca el soporte para el modelo de actores).
Para los primeros, el modelo de actores es un modelo de concurrencia sobre el que centraremos el artículo de hoy. Para los segundos, hoy veremos una posible implementación del mismo en el ecosistema Go.
Pero, ¿qué es el modelo de actores?
Como introducíamos antes, el modelo de actores es un modelo matemático de concurrencia: una definición de un sistema (concurrente) con conceptos matemáticos; o una estrategia bien definida para implementar un sistema concurrente.
[Si tenéis dudas acerca de las diferencias entre concurrencia y paralelismo, podéis revisar nuestro artículo de introducción a la concurrencia en Go]
Pero bueno, dejémonos de tecnicismos y veamos cómo funciona dicho modelo.
La pieza central (la primitiva universal) del modelo son los actores, cuya definición les caracteriza por:
- Pueden tener un estado interno.
- Son capaces de recibir mensajes y actuar en consecuencia (cambiar su estado interno, crear más actores, responder con más mensajes, etc).
Por lo tanto, nos podríamos imaginar un pseudocódigo similar a:
// Inicializamos un actor
actor := actors.New("actorName")
// Definimos las acciones a realizar al recibir un mensaje de un determinado tipo
actor.On("Something", func(...) {
// Do whatever
})
actor2 := actors.New("actorName 2")
actor2.On("Something 2", func(...) {
// Do whatever
})
// Enviamos un mensaje entre dos actores
actor.Tell("Something", actor2)
El modelo en Go
A diferencia de lenguajes como Erlang (o Elixir), cuyo modelo de concurrencia está basado precisamente en el modelo de actores, el modelo de concurrencia de Go está basado en el modelo de comunicación de procesos secuenciales (CSP).
Sin embargo, hacer una implementación del modelo de actores en Go no debería ser excesivamente complicado. De hecho, si nos paramos a pensar un poco sobre la definición que hicimos anteriormente, veremos que para hacer una implementación del mismo necesitamos más bien poco:
- Para el estado interno, podemos tener una estructura de datos (structs) a nuestra conveniencia.
- Para el envío de mensajes entre actores, podemos hacer uso de canales.
- Para la ejecución de múltiples procesos, podemos hacer uso de gorrutinas.
Si, solo con ese pequeño listado de recursos con los que estamos acostumbrados a trabajar en nuestro día a día, ya tendríamos una implementación simple del modelo de actores.
Veamos una implementación muy muy simple:
type actor struct {
ch chan int
}
func (a *actor) Send(n int) {
a.ch <- n
}
func (a *actor) Start() {
go func() {
for x := range a.ch {
fmt.Println(x)
}
}()
}
func (a *actor) Stop() {
close(a.ch)
}
Aunque esto lo podríamos complicar tanto como quisiéramos. Además, tendríamos que tomar algunas decisiones relativamente complejas, como por ejemplo, el tamaño del canal. Es por esta razón, que normalmente los desarrolladores buscamos frameworks o librerías que nos permitan ahorrarnos este tipo de gestiones una y otra vez.
Un framework de actores para Go
Por suerte o por desgracia, y como ya dijimos anteriormente, el framework Akka está enfocado a la JVM y, por lo tanto, no está disponible para Go. Sin embargo, si lo que buscamos es un framework de actores para Go, tenemos alguna que otra alternativa que veremos a continuación.
Pues, como hemos visto, implementar el modelo de actores por encima de CSP, es de lo más sencillo. Pero no nos engañemos, dicha implementación puede ser tan compleja cómo queramos (distribuida, tolerante a fallos, etc).
En esa línea, nos gusta mucho la propuesta de gosiris, el cuál es efectivamente un framework de actores en Go.
Entre sus características resaltan:
- Integración para sistemas distribuidos (mediante AMQP o Kafka).
- Integración para trazabilidad (mediante Zipkin).
- Gestión de jerarquías de actores (estado interno, mailbox, actores secundarios, etc).
- Implementación de patrones de actores (enviar y recibir, supervisión parental, etc).
Ping Pong
De entrada, pinta bien. Sin embargo, lo mejor es que veamos el ejemplo que hemos preparado, representando el Ping Pong (ejemplo habitual en Akka).
En esta ocasión, dos procesos (padre e hijo) se van haciendo ping (y pong) indefinidamente. Usamos time.Sleep(1 * time.Second)
para poder observar
el comportamiento de ambos actores (ver logs de la ejecución) sin tener que entrar en el detalle del ciclo de vida de los actores, aspecto que dejaremos
pendiente de profundizar en otro artículo.
package main
import (
"fmt"
"github.com/teivah/gosiris/gosiris"
"time"
)
func main() {
fmt.Println("Welcome to the Ping Pong gosiris demo")
// Initialize a local actor system
gosiris.InitActorSystem(gosiris.SystemOptions{
ActorSystemName: "ActorSystem",
})
// Create the "ping" actor (parent)
pingActor := gosiris.Actor{}
defer pingActor.Close()
// Create the "pong" actor (child)
pongActor := gosiris.Actor{}
defer pongActor.Close()
// Register a reaction to event types ("message" in this case)
pongActor.React("ping", func(context gosiris.Context) {
context.Self.LogInfo(context, "Ping received from: %s\n", context.Sender.Name())
context.Sender.Tell(gosiris.EmptyContext, "pong", nil, context.Self)
})
// Register a reaction to "pong" messages
pingActor.React("pong", func(context gosiris.Context) {
context.Self.LogInfo(context, "pong received from: %s\n", context.Sender.Name())
context.Sender.Tell(gosiris.EmptyContext, "ping", nil, context.Self)
})
// Register the actors into the system
gosiris.ActorSystem().RegisterActor("pingActor", &pingActor, nil)
gosiris.ActorSystem().SpawnActor(&pingActor, "pongActor", &pongActor, nil)
// Retrieve actor references
pingActorRef, _ := gosiris.ActorSystem().ActorOf("pingActor")
pongActorRef, _ := gosiris.ActorSystem().ActorOf("pongActor")
// Send a message from one actor to another (from pingActor to pongActor)
pongActorRef.Tell(gosiris.EmptyContext, "ping", nil, pingActorRef)
// Just for the example purposes
time.Sleep(1 * time.Second)
}
Y ¡voilà! ¡Ahí lo tenemos!
Pero la cosa no queda aquí. También tenemos otras alternativas, como la implementación para Go de la librería Proto Actor, cuyo enfoque es también muy interesante. Por el momento, os dejamos a vosotros investigar y, en todo caso, lo vemos en artículos futuros.
Y vosotros, ¿ya sabíais lo que es el modelo de actores? ¿ya lo habíais usado?
Como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.