Anuncios

Bienvenidos sean a este post, hoy hablaremos sobre que son y para que nos pueden servir los estados de Lua.

Anuncios

Para crear un nuevo estado debes hacerlo por medio de luaL_newstate o por lua_newstate como cada estado diferente son completamente independiente uno del otro, a su vez estos no comparten datos y esto nos beneficia en que no importa lo que suceda dentro de un estado de Lua, si ocurre algun problema no afectara de ninguna forma a otro estado de Lua pero tampoco nos permite que se comuniquen directamente entre ellos sin embargo esto podemos hacerlo por medio de codigo de C, por ejemplo vamos a suponer que tenemos dos estados, l1 y l2, y vamos a ejecutar el siguiente comando para empujar en l2 una cadena a la parte superior de la pila en l1:

lua_pushstring(l2, lua_tostring(l1, -1))

Como esta informacion debe pasar a traves de C los estados de Lua solo pueden intercambiar tipos que pueden ser representados en este lenguaje como cadenas y numeros.

Anuncios

En sistemas que ofrecen multithreading o multihilos, una forma interesante para usarlos con Lua es crear un estado independiente para cada thread o hilo, esta arquitectura resulta en threads similares a los procesos de Unix donde tenemos concurrencia sin memoria compartida, la idea es crear una implementacion prototipo para multithreading siguiendo este enfoque, para esta ocasion usaremos threads de POSIX (pthreads) para esta implementacion, esto no deberia dificultar poder cambiar a este codigo para otro sistemas de threads.

Anuncios

El sistema que desarrollaremos es muy simple, su proposito principal es mostrar el uso de multiplos estados de Lua en un contexto de multithreading, una vez que lo terminemos y dejemos corriendo podemos agregar mas caracteristicas avanzadas en su parte superior, para esto llamaremos a la libreria lproc la cual nos ofrece cuatro funciones:

  • lproc.start(chunk), inicia un nuevo proceso para ejecutar el chunk informado, un proceso de Lua es implementado como un thread de C mas su estado de Lua asociado.
  • lproc.send(canal, val1,val2,…,valN), envia todos los valores informados al canal informado, en todos los casos debe ser de tipo string.
  • lproc.receive(canal), recibe los valores enviado al canal informado
  • lproc.exit(), finaliza un proceso
Anuncios

En la ultima funcion solo el proceso principal necesita de esta funcion porque si este proceso termina sin llamar a lproc.exit, el programa entero termina sin esperar el final de los otros procesos.

Anuncios

Los canales que mencionamos en las funciones son simplemente cadenas usadas para coincidir al transmisor y el receptor, como vimos la operacion de envio (send) puede enviar todos los valores que necesitemos (siempre de tipo string) los cuales son devueltos por la coincidencia de operacion de recepcion (receive), toda la comunicacion es sincronica es decir que un proceso enviando un mensaje a un canal se bloquea hasta que haya un proceso de recepcion de este canal y mientras un proceso esta recibiendo de un canal se bloquea hasta que haya un proceso enviandolo, asi como las interfaces del sistema las implementaciones son muy simples, ya que usa dos listas circulares de doble enlace porque uno es para procesos de espera para enviar un mensaje y otro para los procesos de espera para recibir el mensaje porque este usa una simple exclusion mutua (mutex) para controlar el acceso a estas listas, cada proceso tiene una variable de condicion asociada y cuando un proceso quiere enviar un mensaje a un canal este atraviesa la lista de recepcion en busqueda de un proceso esperando por dicho canal, si lo encuentra se remueve el proceso de la lista de espera, mueve los mensajes de si mismo al proceso encontrado y señala los otros procesos, de lo contrario se inserta a si mismo dentro de la lista de espera y espera por su variable de condicion, para recibir un mensaje se hace una operacion simetrica, el elemento principal en la implementacion es la estructura que representa un proceso:

struct Proc
{
    lua_State *L;
    pthread_t thread;
    pthread_cond_t cond;
    const char *canal;
    struct Proc *previo, *proximo;
} Proc;
Anuncios

Los dos primeros campos representan al estado de Lua por el proceso de Lua y el thread de C que corre el proceso, los otros campos son usados solamente cuando el proceso tiene que esperar por las coincidencias de emision/recepcion, el tercer campo (cond) es la variable de condicion que el thread usa para bloquear a si mismo, el cuarto campo almacena el canal que el proceso esta esperando, y los ultimos dos campos (previo y proximo) son los usados para enlazar el proceso en la lista de espera, las dos listas de espera y las mutex asociadas son declaradas de la siguiente forma:

static Proc *esperaenv = NULL;
static Proc *esperarec = NULL;

static pthread_mutex_t acceso_kernel = PTHREAD_MUTEX_INITIALIZER;
Anuncios

Como cada proceso necesita a la estructura Proc y esta a su vez necesita a su estructura cuando su script llama a send o receive, como el unico parametro que estas funciones recibe es el proceso del estado de Lua, hace que cada proceso deberia almacenar su estructura Proc dentro de su estado de Lua, por ejemplo como un userdata completo en el registro, en nuestra implementacion cada estado mantiene su correspondiente estructura Proc en el registro asociado con la clave “_SELF“.

Anuncios

La funcion getself recupera la estructura Proc asociado con un estado informado:

static Proc *getself (lua_State *L)
{
    Proc *p;
    lua.getfield(L, LUA_REGISTRYINDEX, "_SELF");
    p = (Proc *)lua_touserdata(L, -1);
    lua_pop(L, 1);
    return p;
}
Anuncios

La siguiente funcion es movervalores y sera para mover los valores de un proceso transmisor a un proceso receptor:

static void movervalores (lua_State *enviar, lua_State *rec)
{
    int n = lua_gettop(enviar);
    int i;
    for(i = 2; i <= n; i++)
        lua_pushstring(rec, lua_tostring(enviar, i));
}
Anuncios

Esta funcion mueve al receptor todos los valores en la pila del transmisor excepto el primero que es el canal, veamos la siguiente funcion:

static Proc *buscacoinc (const *char canal, Proc **lista)
{
    Proc *nodo = *lista;
    if (nodo == NULL) return NULL;
    do
    {
        if (strcmp(canal, nodo->canal) == 0)
        {
            if (*lista == nodo)
                *lista = (nodo->proximo == nodo) ? NULL : nodo->proximo;
            nodo->previo->proximo = nodo->proximo;
            nodo->proximo->previo = nodo->previo;
            return nodo;
        }
        nodo = nodo->proximo;
    } while(nodo != *lista);
    return NULL;
}
Anuncios

Esta funcion que llamamos buscacoinc se encargara de atravesar la lista de espera buscando un proceso en la espera de un canal informado, donde primero creara el nodo y le asignara el valor informado en lista, luego chequea si nodo es igual a NULL lo cual implica que la lista puede estar vacia y por ende devolvera NULL directamente.

Lo siguiente es un bucle do-while donde lo ejecutara mientras nodo sea distinto de lista, en el bloque tenemos primero un condicional donde verifica si hubo coincidencia, luego tendremos un condicional donde verifica si es el primer nodo, en caso de ser cierto devolvera un NULL sino obtendra el valor de proximo.

Las siguientes dos lineas obtienen los valores correspondientes y por ultimo devuelve el valor de nodo, despues obtenemos el valor de proximo y lo asignamos a nodo, por ultimo cuando salimos del condicional devuelve NULL, como se daran cuenta esta funcion lo unico que hace es buscar un proceso de la lista, si lo encuentra lo remueve y lo devuelve, de lo contrario devuelve NULL, pasemos a ver nuestra siguiente funcion:

static void esperaenlista (lua_State *L, const char *canal, Proc **lista)
{
    Proc *p = getself(L);
    if (*lista == NULL)
    {
        *lista = p;
        p->previo = p->proximo = p;
    }
    else
    {
        p->previo = (*lista)->previo;
        p->proximo = *lista;
        p->previo->proximo = p->proximo->previo = p;
    }
    p->canal = canal;
    do
    {
        pthread_cond_wait(&p->cond, &acceso_kernel);
    } while(p->canal);
}
Anuncios

Esta nueva funcion auxiliar es llamada cuando un proceso no puede encontrar una coincidencia, en este caso este proceso se engancha a si mismo al final de la lista de espera apropiada y espera hasta que otro proceso coincida con este y lo despierta.

Nota: El bucle alrededor pthread_cond_wait lo protege de falsos despertares permitidos en threads de POSIX.
Anuncios

Cuando un proceso despierta a otro, este establece el otro canal de campo de proceso en NULL, asi que si p->canal no es NULL esto significa que nadie coincidio con el proceso y debes seguir esperando, ahora que tenemos nuestras funciones auxiliares podemos proceder a escribir las funciones de send y receive:

static int ll_send(lua_State *L)
{
    Proc *p;
    const char *canal = luaL_checkstring(L, 1);
    pthread_lock_mutex(&acceso_kernel);
    p = buscacoinc(canal, &esperarec);
    if (p)
    {
        movervalores(L, p->L);
        p->canal = NULL;
        pthread_cond_signal(&p->cond);
    }
    else
    {
        esperaenlista(L, canal, &esperaenv);
        pthread_mutex_unlock(&acceso_kernel);
        return 0;
    }
}

static int ll_receive(lua_State *L)
{
    Proc *p;
    const char *canal = luaL_checkstring(L, 1);
    lua_settop(L, 1);
    pthread_mutex_lock(&acceso_kernel);
    p = buscacoinc(canal, &esperaenv);
    if (p)
    {
        movervalores(p->L, L);
        p->canal = NULL;
        pthread_cond_signal(&p->cond);
    }
    else
    {
        esperaenlista(L, canal, &esperarec);
        pthread_mutex_unlock(&acceso_kernel);
        return lua_gettop(L) - 1;
    }
}
Anuncios

En este caso tenemos las dos funciones, primero hablemos de la encargada de enviar, ll_send, la cual primero chequea por el canal luego bloque el mutex y busca por un receptor coincidente, si encuentra uno mueve sus valores a este receptor, marca al receptor como listo y lo despierta, de lo contrario se pone a si mismo en espera, una vez finalizada la operacion desbloquea el mutex y regresa sin valores a Lua, la funcion receptora, ll_receive, es similar pero devuelve todos los valores recibidos, si observan ambas estructuras son identicas se modifican el dato a devolver al final, no devuelve 0 sino la parte superior de la pila menos uno y en lugar de usar la espera para enviar usa la de para recibir despues son exactamente los mismos, para nuestro siguiente paso veremos como crear los nuevos procesos porque un nuevo proceso necesita de un nuevo thread y a su vez un nuevo thread necesita un cuerpo pero de este cuerpo hablaremos luego, a continuacion tenemos un prototipo siguiendo las reglas de threads de POSIX:

static void *ll_thread(void *arg);
Anuncios

Para crear y correr un nuevo proceso el sistema debe crear un nuevo estado de Lua, iniciar un nuevo thread, compilar el chunk informado, llamar al chunk y por ultimo liberar sus recursos, de las tres primeras tareas se encarga el thread original y el nuevo thread se encarga del resto.

Nota: Para simplificar el manejo de errores el sistema solo inicia el nuevo thread despues que se compilo exitosamente el chunk informado.
Anuncios

Para crear un nuevo estado vamos a usar una funcion llamada ll_start, su codigo es el siguiente:

static int ll_start(lua_State *L)
{
    pthread_t thread;
    const char *chunk = luaL_checkstring(L, 1);
    lua_State *L1 = luaL_newstate();
    if (L1 == NULL)
        luaL_error(L, "no se puede crear un nuevo estado");
    if (luaL_loadstring(L1, chunk) != 0)
        luaL_error(L, "error iniciando thread: %s",
                    lua_tostring(L1, -1);
    if (pthread_create(&thread, NULL, ll_thread, L1) != 0)
        luaL_error(L, "no se puede crear un nuevo thread");
    pthread_detach(thread);
    return 0;
}
Anuncios

Esta funcion crea un nuevo estado de Lua llamado L1 y compila en este la chunk informada, en caso de error señala a este al estado original L luego crea un nuevo thread (pthread_create) con cuerpo a traves de ll_thread pasando al nuevo estado L1 como argumento para el cuerpo, la llamada a pthread_detach le dice al sistema que no quiere ninguna respuesta final de este thread, para crear el cuerpo de nuestro nuevo thread dijimos que debemos usar a la funcion ll_thread, el codigo es el siguiente:

static void ll_thread(void *arg)
{
    lua_State *L = (lua_State *)arg;
    luaL_openlibs(L);
    lua_cpcall(L, luaopen_lproc, NULL);
    if (lua_pcall(L,0,0,0) != 0)
        fprintf(stderr, "thread error: %s", lua_tostring(L, -1));
    pthread_cond_destroy(&getself(L)->cond);
    lua_close(L);
    return NULL;
}
Anuncios

Esta funcion recibe su correspondiente estado de Lua desde ll_start con solo el chunk principal precompilado en la pila, el nuevo thread abre las librerias estandard de Lua, abre la libreria lproc y luego llama a su chunk principal, por ultimo destruye su variable de condicion (la cual fue creada por luaopen_lproc) y cierra su estado de Lua, la ultima funcion del modulo es exit y esta es bien simple como se ve a continuacion:

static int ll_exit(lua_State *L)
{
    pthread_exit(NULL);
    return 0;
}
Anuncios

Recordemos que solo el proceso principal necesita llamar a esta funcion cuando finaliza, para evitar el inmediato final del programa entero, nuestro ultimo paso es definir la funcion de apertura para el modulo lproc, su codigo es el siguiente:

static const struct luaL_reg ll_funcs[] = {
    {"start", ll_start},
    {"send", ll_send},
    {"receive", ll_receive},
    {"exit", ll_exit},
    {NULL, NULL}
}

int luaopen_lproc (lua_State *L)
{
    Proc *self = (Proc *)lua_newuserdata(L, sizeof(Proc));
    lua_setfield(L, LUA_REGISTRYINDEX, "_SELF");
    self->L = L;
    self->thread = pthread_self();
    self->canal = NULL;
    pthread_cond_init(&self->cond, NULL);
    luaL_register(L, "lproc", l_funcs);
    return 1;
}
Anuncios

Esta funcion debe registrar las funciones de modulo, como es usual pero tambien tiene que inicializar y crear la estructura Proc del proceso que esta corriendo, a continuacion veremos algunas mejoras que podemos hacer en esta implementacion.

Anuncios
Anuncios

Una mejora mas que obvia es cambiar la busqueda lineal por una coincidencia de canal, y una buena alternativa es usar una tabla de hash para encontrar un canal y usar listas de espera independiente para cada canal, otra mejora puede girar alrededor de la eficiencia a la hora de crear los procesos porque la creacion de nuevos estados de Lua es una operacion liviana sin embargo, la apertura de todas las librerias estandard toma mas de diez veces el tiempo para abrir que la creacion de un estado, la mayoria de los procesos probablemente no necesitaran todas las mismas, en realidad la mayoria necesitara una o dos librerias, si bien podemos evitar el costo de abrir una libreria usando la preregistracion de librerias, de esto hablamos en este post.

Con este enfoque en lugar de llamar a la funcion luaopen_* para cada libreria estandar solo ponemos esta funcion dentro de la tabla package.preload, esto hace que si el proceso llama a require “lib”, y solamente en este caso, la funcion require llamara a la funcion asociada para abrir la libreria, la siguiente funcion hace la registracion:

static void registerlib (lua_State *L, 
                            const char *nombre, 
                            lua_CFunction f)
{
    lua_getglobal(L, "package");
    lua_getfield(L, -1, "preload");
    lua_pushcfunction(L, f);
    lua_setfield(L, -2, nombre);
    lua_pop(L, 2);
}
Anuncios

Una buena practica es abrir la libreria basica aunque tambien se necesita la libreria package porque de lo contrario no tendras disponibles a require para abrir otras librerias pero como todas las librerias pueden ser opcionales en lugar de usar a lua_openlibs podemos llamar a la siguiente funcion openlibs cada vez que creamos un nuevo estado:

static void openlibs (lua_state *L)
{
    lua_cpcall(L, luaopen_base, NULL);
    lua_cpcall(L, luaopen_package, NULL);
    registerlib(L, "io", luaopen_io);
    registerlib(L, "os", luaopen_os);
    registerlib(L, "table", luaopen_table);
    registerlib(L, "string", luaopen_string);
    registerlib(L, "math", luaopen_math);
    registerlib(L, "debug", luaopen_debug);
}
Anuncios

La primera linea se encarga de cargar la libreria basica, la segunda se encarga de cargar la libreria de package, despues registrara algunas librerias para nuestro uso, asi que cuando un proceso necesite alguna de estas librerias, es decir que requiera algunas de ellas explicitamente, y hara que require llame a la funcion luaopen_* correspondiente, otra de las mejoras que podemos realizar envuelve a las comunicaciones primarias.

Anuncios

Por ejemplo, seria muy util proveer limites de cuanto tiempo puede esperar por una coincidencia a lproc.send y lproc.receive, como un caso particular podriamos a hacer que un limite cero haga que estas funciones no se bloqueen, y con los threads POSIX se puede implementar esta habilidad usando a pthread_cond_timewait.

Anuncios

En resumen, hoy hemos visto que son los estados de Lua, como se relacionan con los threads, asi como los procesos del sistema, hemos visto un ejemplo en POSIX donde paso a paso hemos creado todas las funciones necesarias y por ultimo hemos visto algunas mejoras que se pueden efectuar sobre el codigo, espero les haya sido util sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.

Anuncios

Tengo un Patreon donde podes acceder de manera exclusiva a material para este blog antes de ser publicado, sigue los pasos del link para saber como.

Tambien podes donar

Es para mantenimiento del sitio, gracias!

$1.00