Desde hace tiempo quiero escribir esta entrada, pero por falta de
tiempo no he podido. El mecanismo de actores se utiliza en lenguajes
de programación como Erlang y Scala para sincronizar diferentes
«actores» que están funcionando en el sistema. Tradicionalmente, la
programación con hilos (salvo en casos como BSP, por ejemplo) se ha
realizado básicamente como la programación monoproceso, pero haciendo
que el programador tuviera en la cabeza las posibles colisiones que
varios hilos ejecutando un código podrían tener.
La otra cara
de esta moneda la han tenido lenguajes y paradigmas que cambiaban la
manera de programación hacia esquemas que hicieran más fácil escalar
en el número de hilos/cores a la vez que permitían una programación
más natural de programas multihilo. Estos nuevos paradigmas también
evitaban, por diseño, los problemas que se dan con los candados,
reentrancia, etc.
El paradigma sobre el que hablaré hoy es el
de los actores. Este mecanismo, que data de 1986, se utiliza en Erlang
y en Scala,
por ejemplo, pero no he encontrado ejemplos de cómo implementar este
mecanismo en C++, salvo un artículo
de 1993 de Kafura, Mukherji y Lavender en el que no se hace uso de
ninguna característica «moderna» de C++, como los tipos parametrizados
o la sobrecarga de operadores.
En resumen, el mecanismo de actores se basa en definir un actor
como un objeto reactivo que se ejecuta en su propio hilo. Son
similares a los objetos stricto sensu, en el sentido de que
se les puede enviar invocaciones (en mi caso eventos), y los actores
responden ejecutándolos, como los objetos normales. No obstante, son
diferentes porque las invocaciones se ejecutan de manera que no causan
problemas de concurrencia. ¿Cómo? Pues asegurando que todas las
invocaciones sobre un actor se ejecutan en un mismo hilo. En este
sentido, un actor también aglutina, en general, un hilo de ejecución
propio en el que se ejecutan las llamadas al mismo (esto puede no ser
así exactamente, pero la idea es la misma).
Existe una
diferencia con los paradigmas tradicionales de programación. Por
ejemplo, para no causar problemas de concurrencia, todos los métodos
de un objeto se podrían marcar como «synchronized
» al
estilo de Java. Esto, efectivamente, hace que no haya problemas de
concurrencia (al menos los más usuales), ya que todas las invocaciones
a un objeto se realizan en exclusión mutua. Sin embargo, una
invocación a objeto normal lleva consigo asociada un hilo de
ejecución, y el hilo de ejecución del objeto llamante es el que
realiza la llamada al objeto llamado, con lo que también se tienen que
prevenir problemas como interbloqueos, esperas de candados, etc.
En resumen, sería casi como un sistema basado en eventos en donde
los objetos se envían mensajes que son a su vez procesados en los
hilos respectivos de cada actor. Ahora bien, ¿cómo implementar en C++
este mecanismo sin ser excesivamente intrusivo, teniendo en cuenta que
el mecanismo de envío de eventos no existe en C++? Pensé en utilizar
boost.signal
, pero éste no asegura que el objeto receptor
va a ejecutar la señal en su propio hilo. Los requisitos que establecí
pues para el desarrollo fueron los siguientes:
- El
mecanismo debe ser poco intrusivo, en el sentido de que las clases que
quieran beneficiarse de este mecanismo no tienen por qué escribirse
heredando de un interfaz en particular, sino que sólo tienen que
definir una serie de tipos para saber tratarlas como actor.
- Cualquier clase puede definir de manera sencilla qué eventos puede
recibir y cómo actuará ante cualquier evento, y estos serán los únicos
requisitos que tendrá que especificar la clase.
- Las clases pueden modificar de forma sencilla qué eventos producen y reciben.
- Las clases no tendrán que preocuparse de tratar con hilos, asincronía, almacenamiento y reproducción de eventos, etc.
- El mecanismo de envío de eventos debe estar integrado en el lenguaje C++ de forma natural. Por ejemplo, con un operador que muestre que se está enviando un evento:
objeto << mensaje;
.
Con estos requisitos, pensé hacer la clase actor
que pudiera aceptar como parámetro cualquier otra clase, y convertirla así en un actor. Este mecanismo es poco intrusivo, sólo obligando a que la clase que se quiere beneficiar de este mecanismo especifique qué eventos es capaz de recibir. La clase actor que me quedó fue la siguiente, con comentarios al estilo de la literate programming (si alguien está interesado le puedo pasar el código sin los comentarios):
template <typename Klass>
struct actor
{
typedef typename Klass::events_type VariantType;
Uno de los requisitos que tiene que proveer la clase que se va a convertir en actor es ofrecer el tipo VariantType
con los distintos eventos que va a poder recibir. Para esto se usará el tipo boost.variant
como se verá después.
typedef actor<Klass> self;
actor (Klass& a)
: delegate_ (a)
{
thread_ = boost::thread (boost::ref (*this));
}
Cada actor posee su propio hilo. Esto, por supuesto puede modificarse después. Sólo quería hacer una prueba de concepto. En scala existen schedulers que enlazan actores con hilos.
// Thread func.
void operator()()
{
std::cout << “running thread” << std::endl;
while(!stop_)
{
bool b;
{
boost::lock_guard<boost::mutex> guard (list_mutex_);
b = el_.empty();
}
if (b)
{
boost::unique_lock<boost::mutex> lock (mut_);
// wait on the cond. var.
cond_.wait (lock);
}
while (true)
{
VariantType vtv;
{
boost::lock_guard<boost::mutex> guard (list_mutex_);
if (el_.empty())
break;
vtv = el_.front();
el_.pop_front();
}
// Call the delegate without holding the mutex locked
boost::apply_visitor (detail::event_caller<Klass> (delegate_),
vtv);
}
}
}
El operator()()
de la clase actor ejecuta el código del hilo. He utilizado variables de condición porque me parecen más ricas semánticamente. El hilo básicamente extrae eventos de la cola de eventos y los ejecuta sobre el delegate. Como los eventos de la cola pueden ser de diferentes tipos (nótese que este punto es especialmente difícil en C++), hay que utilizar estructuras que permitan tratar diferentes tipos de eventos de forma genérica. Para eso he usado la construcción boost::apply_visitor
de boost.variant
. Con el uso de una clase especial, detail::event_caller
, que se verá más abajo, se consigue llamar a la clase original, a los métodos process(Evento)
, para cada uno de los eventos recibidos.
template <typename Event>
self& operator<<(Event& e)
{
std::cout << “Received event” << std::endl;
{
boost::lock_guard<boost::mutex> lock (list_mutex_);
el_.push_back (e);
}
// signal that a new event is available
cond_.notify_one();
return *this;
}
El operador <<
se puede usar para enviar un evento al actor. Esta es una construcción que queda muy natural. Enviar un mensaje es diferente a realizar una llamada, aunque también se puede pensar en un mecanismo de llamada a función modificado. Al final, el envío de mensajes, como se verá después, será algo así como actor << Clase::Evento(valores);
.
void join()
{
thread_.join();
}
void stop()
{
stop_ = true;
cond_.notify_one();
}
private:
Klass& delegate_;
typedef std::deque<VariantType> event_list;
event_list el_;
bool stop_; // stop?
boost::thread thread_;
boost::mutex mut_;
boost::mutex list_mutex_;
boost::condition_variable cond_;
};
Por completitud, aquí está la clase detail::event_caller
. Es necesaria para visitar un tipo boost.variant
a través de la función boost::apply_visitor
. Simplemente llama a la función process()
correspondiente.
namespace detail
{
template <typename Klass>
struct event_caller : public boost::static_visitor<>
{
Klass& instance_;
event_caller (Klass& i)
: instance_ (i)
{
}
template <typename T>
void operator()( T const & operand ) const
{
instance_.process (operand);
}
};
}
Llegamos a la clase sobre la que queremos construir el actor, llamada para este ejemplo TestClass
. La clase define internamente un par de eventos (Event1
y Event2
), y, como comentamos arriba, el tipo events_type
, como un boost.variant
de los diferentes eventos que puede recibir. Se pueden ver los métodos process()
más abajo. En este caso la clase tiene métodos propios para retornar un actor interno. Esto no tiene por qué ser así, y como se ha visto, los actores son independientes de las clases de las que actúan en representación.
class TestClass
{
public:
typedef actor<TestClass> actor_type;
struct Event1
{
int data;
};
struct Event2
{
std::string ss;
};
// Obligatory
typedef boost::variant< Event1, Event2 > events_type;
actor_type& the_actor()
{
return *actor_;
}
// Ctor.
TestClass()
: actor_ (new actor_type (*this))
{
}
~TestClass()
{
actor_->stop();
actor_->join();
delete actor_;
}
private:
actor_type* actor_;
public:
void process (Event1 const& e)
{
std::cout << “Processed event: “ << e.data << std::endl;
}
void process (Event2 const& e)
{
std::cout << “Processed event 2: “ << e.ss << std::endl;
}
};
Por último, ¿cómo se usa este mecanismo de actores? Lo ideal es proveer de mecanismos que sean semánticamente ricos y que sigan el principio de mínima sorpresa. Con las clases de arriba podemos escribir código sencillo como el siguiente:
TestClass::Event1 ev;
ev.data = -2;
TestClass::Event2 ev2;
ev2.ss = “abcdef.”;
TestClass tc;
TestClass::actor_type& ac = tc.the_actor();
Primero se crean un par de eventos de los dos tipos que puede recibir la clase TestClass
, y se obtiene el actor ac
. Se puede usar ese actor para enviar eventos a la clase:
// Send the event2
ac << ev2;
// Send message
ac << ev;
for (int i = 0; i < 2000; ++i)
{
ev.data = i;
ac << ev;
ac << ev2;
}
Aquí se envía primero un evento de tipo Event1
, y luego otro del tipo 2. Después se entra en un bucle que envía ambos mensajes, modificando el primer evento con un dato distinto. El programa va mostrando la salida de eventos de la clase en orden:
...
Processed event: 1996
Processed event 2: abcdef.
Processed event: 1997
Processed event 2: abcdef.
Processed event: 1998
Processed event 2: abcdef.
Processed event: 1999
Processed event 2: abcdef.
Un último apunte. Las diferencias con los actores de otros lenguajes dinámicos (Scala, por ejemplo), son que en estos lenguajes se puede especificar un procesado basado en máquinas de estados, como por ejemplo, cuando se recibe el evento 1, después sólo se puede recibir el evento 2. Esta máquina de estados se puede implementar. Es una primera implementación de prueba.
No dudéis en contactar conmigo para ideas o comentarios.