En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 16 de septiembre de 2017

Thread Ringers
cómo usar los thread en C - pt.2

¿Donde lo dejamos? Ah, sí: en la primera parte de Dead Ringers (oops... Thread Ringers) hemos introducido el argumento thread hablando de la base, es decir, los POSIX Threads. Ahora, como prometido, vamos a tratar de escribir el mismo ejemplo de el último post utilizando una interfaz alternativa, o sea los C11 Threads.
...te lo explicaré: yo soy un POSIX thread y tu un C11 thread...
Una premisa que parte del lado oscuro de la fuerza (bueno, el C++): el committee ISO del C++ decidió introducir en la versión C++11, los thread dentro del lenguaje. Por lo tanto, no más uso directo de los POSIX Threads a través de (por ejemplo) la librería libpthread, sino el uso directo de constructos del lenguaje mismo. El resultado final ha sido (en mi opinión) brillante, y los C++11 Threads son una de las pocas cosas que uso frecuentemente del C++11 (y ya sabéis lo que pienso del mal camino tomado por el C++ por culpa del committee ISO, y si no ir a leer ese post). El committee ISO del C no ha podido quedarse atrás, por lo que han pensado de hacer lo mismo con el C11, por lo que los thread son ahora directamente parte del C... o no? Os adelanto una consideración: aprecio el committee ISO del C muchísimo más que el committee ISO del C++ (esto era evidente...), pero en este caso me parece que algo no ha funcionado bien: a seguir veremos por qué .

¿Cómo se han realizado los nuevos C11 Threads? Vale, han tomado todas las funciones y variables que componen los POSIX Threads y le han cambiado el nombre (y tengo que admitir que los nuevos son más simples); además, en algunos casos (pocos, afortunadamente), han cambiado los tipos de los códigos de retorno y de los argumentos de las funciones. Punto. ¿Brillante? No exactamente, diría, y nada que ver con la brillante solución utilizada en C++11. ¿Razones para usar esta nueva versión? Cero, diría, y todavía no he expuesto el problema principal...

De todos modos, he reescrito el ejemplo del último post usando los C11 Threads. ¡Vamos con el código!


#include <stdio.h>
#include <threads.h>
#include <string.h>
#include <unistd.h>

// creo un nuevo tipo parar pasar datos a los thread
typedef struct _tdata {
    int   index;      // thread index
    int   *comdata;   // dato común a los thread
    mtx_t *lock;      // mutex común a los thread
} tdata;

// prototipos locales
int tMyThread(void *arg);

// función main()
int main(int argc, char* argv[])
{
    int error;

    // init mutex
    mtx_t lock;
    if ((error = mtx_init(&lock, mtx_plain)) != thrd_success) {
        printf("%s: no puedo crear el mutex (error=%d)\n", argv[0],  error);
        return 1;
    }

    // init threads
    thrd_t tid[2];
    tdata  data[2];
    int    comdata = 0;
    for (int i = 0; i < 2; i++) {
        // set data del thread y crea el thread
        data[i].index   = i;
        data[i].comdata = &comdata;
        data[i].lock    = &lock;
        if ((error = thrd_create(&tid[i], tMyThread, &data[i])) != thrd_success)
            printf("%s: no puedo crear el thread %d (error=%d)\n", argv[0], i, error);
    }

    // join threads y borra mutex
    thrd_join(tid[0], NULL);
    thrd_join(tid[1], NULL);
    mtx_destroy(&lock);

    // exit
    printf("%s: thread acabados: comdata=%d\n", argv[0], comdata);
    return 0;
}

// thread routine
int tMyThread(void *arg)
{
    // obtengo los datos del thread con un cast (tdata*) de (void*) arg
    tdata *data = (tdata *)arg;

    // thread loop
    printf("thread %d comenzado\n", data->index);
    int i = 0;
    for (;;) {
        // lock mutex
        mtx_lock(data->lock);

        // incrementa comdata
        (*data->comdata)++;

        // unlock mutex
        mtx_unlock(data->lock);

        // test counter parar eventual salida del loop
        if (++i >= 100) {
            // sale del loop
            break;
        }

        // thread sleep (10 ms)
        usleep(10000);
    }

    // el thread sale
    printf("thread %d acabado\n", data->index);
    return 0;
}

Como se puede ver el código es prácticamente lo mismo, me he limitado a utilizar las nuevas funciones en lugar de las viejas (por ejemplo, thrd_create() en lugar de pthread_create()), he utilizado los nuevos tipos (por ejemplo, mtx_t en lugar de pthread_mutex_t) y he ligeramente modificado el test de los valores de retorno: pocas diferencias, tengo que decir, y, en algunos casos, a peor: por ejemplo, ha desaparecido el parámetro attr de pthread_create(), que (por simplicidad) en el último ejemplo había dejado a NULL, pero que a veces puede ser útil (leer el manual de pthread_create() para darse cuenta). Sin embargo, se podría decir (sin ser demasiados exigentes) que la nueva interfaz no nos ofrece ninguna ventaja sustancial, pero ni siquiera un deterioro decisivo, por lo que también se podría utiliza (de gustibus).

Pero hay un problema: parece que los C11 Threads no son considerados una prioridad para los que escriben los compiladores y las libc, por lo que actualmente es difícil compilar/ejecutar un programa como el que he enseñado. Incluso nuestro amado GCC (que suele ser el primero en dar soporte a las últimas novedades) no soporta los nuevos thread (en realidad debido a la falta de integración en la glibc). Por lo tanto, si realmente deseáis usarlos a toda costa, tendréis que esperar a que algún compilador/librería proporcione el soporte completo o bien, por ejemplo, utilizar la librería c11threads, que no es más que un wrapper que simula los C11 Threads utilizando los POSIX Threads.

Yo, al final, he compilado el ejemplo usando lo que (creo) es la solución más interesante actualmente disponible: he instalado en mi sistema la musl libc que es una libc alternativa a la glibc, y tiene un wrapper para GCC (musl-gcc): musl proporciona (en Linux) el soporte completo a C11, thread incluidos. Una vez compilado, el programa se comporta correctamente, como se puede ver a continuación:

aldo@ao-linux-nb:~/blogtest$ musl-gcc c11thread.c -o c11thread
aldo@ao-linux-nb:~/blogtest$ ./c11thread 
thread 0 comenzado
thread 1 comenzado
thread 1 acabado
thread 0 acabado
./c11thread: thread acabados: comdata=200

¿Pero vale la pena hacer este cambio? No, por mi parte seguiré utilizando los POSIX Threads, que utilizo desde hace años y siguen siendo la referencia de excelencia. Y una última consideración: independientemente de lo que estamos utilizando (C11/C++11 threads) es muy probable que, por debajo, haya los POSIX Threads (esto es cierto en muchas implementaciones). Y si cuando compiláis tenéis que añadir el flag -pthread entonces la duda se convierte en una certeza, ya que con este flag vais a usar libpthread o sea la librería POSIX Threads. Sorpresa, sorpresa...

¡Hasta el próximo post!

viernes, 25 de agosto de 2017

Thread Ringers
cómo usar los thread en C - pt.1

Los thread son un poco como los gemelos de la obra maestra de David Cronenberg: tienen el mismo origen, parecen iguales pero son diferentes.
...te lo explicaré: yo soy el thread A y tu eres el B...
En este post (que es el primero de una serie corta) veremos un ejemplo muy simple de cómo usar los thread en C: obviamente el tema es vasto y se puede hacer mas complicado a voluntad, pero nuestro ejemplo ya contiene los conceptos básicos para entender cómo funciona todo, o sea: la creación, la sincronización y la destrucción de los thread. Obviamente empezaremos usando la versión de base (casi) universal, es decir, usaremos los POSIX Threads. Y ahora vamos al grano, ¡vamos con el código!

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

// creo un nuevo tipo parar pasar datos a los thread
typedef struct _tdata {
    int             index;      // thread index
    int             *comdata;   // dato común a los thread
    pthread_mutex_t *lock;      // mutex común a los thread
} tdata;

// prototipos locales
void* tMyThread(void *arg);

// función main()
int main(int argc, char* argv[])
{
    int error;

    // init mutex
    pthread_mutex_t lock;
    if ((error = pthread_mutex_init(&lock, NULL)) != 0) {
        printf("%s: no puedo crear el mutex (%s)\n", argv[0],  strerror(error));
        return 1;
    }

    // init threads
    pthread_t tid[2];
    tdata     data[2];
    int       comdata = 0;
    for (int i = 0; i < 2; i++) {
        // set data del thread y crea el thread
        data[i].index   = i;
        data[i].comdata = &comdata;
        data[i].lock    = &lock;
        if ((error = pthread_create(&tid[i], NULL, &tMyThread, (void *)&data[i])) != 0)
            printf("%s: no puedo crear el thread %d (%s)\n", argv[0], i, strerror(error));
    }

    // join threads y borra mutex
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_mutex_destroy(&lock);

    // exit
    printf("%s: thread acabados: comdata=%d\n", argv[0], comdata);
    return 0;
}

// thread routine
void* tMyThread(void *arg)
{
    // obtengo los datos del thread con un cast (tdata*) de (void*) arg
    tdata *data = (tdata *)arg;

    // thread loop
    printf("thread %d comenzado\n", data->index);
    int i = 0;
    for (;;) {
        // lock mutex
        pthread_mutex_lock(data->lock);

        // incrementa comdata
        (*data->comdata)++;

        // unlock mutex
        pthread_mutex_unlock(data->lock);

        // test counter parar eventual salida del loop
        if (++i >= 100) {
            // sale del loop
            break;
        }

        // thread sleep (10 ms)
        usleep(10000);
    }

    // el thread sale
    printf("thread %d acabado\n", data->index);
    return NULL;
}

Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales. Suponiendo que ya sabeis lo que son y para qué sirven los thread (si no leer algunas guías introductorias, hay algunas muy buenas en la red) el flujo de código es obvio: primero hay que crear un mutex (con pthread_mutex_init()) para sincronizar los thread que vamos a utilizar, entonces hay que inicializar los datos que hay que pasar a los thread y crear (con pthread_create()) los dos thread de nuestro ejemplo (init de datos y creación los he puesto en un loop de 2, pero también se hubiera podido escribir en dos pasos, obviamente). Finalmente el main() se pone a la espera (con pthread_join()) de la terminación de los thread y, cuando terminan, destruye el mutex (con pthread_mutex_destroy()) y sale.

Como se puede ver pthread_create() tiene cuatro parámetros, que son (en el orden): un pointer a un thread descriptor (que identifica de forma exclusiva el thread creado), un pointer a un contenedor de atributos del thread a crear, un function pointer a la función que ejecutará el thread y, finalmente, un pointer al único argumento que se puede pasar a la función anterior. Específicamente, en nuestro ejemplo (muy simple), he usado los atributos por defecto (usando NULL para el segundo parámetro), y he creado (con typedef) un nuevo tipo ad-hoc para pasar múltiples parámetros a la función que ejecutará el thread, explotando el hecho de que el argumento de función por defecto es un void* que puede ser fácilmente transformado (con una operación de cast) a cualquier tipo complejo (en nuestro caso el nuevo tipo tdata).

En este ejemplo, los dos thread creados ejecuta la misma función, tMyThread() (pero también podrían realizar dos funciones completamente diferentes: en este caso, por supuesto, hubiera tenido que escribir una tMyThread1() y una tMyThread2 ()). El flujo de la función es muy simple: primero ejecuta un cast sobre el argumento arg para utilizar los datos del tipo tdata, luego entra en un clásico thread-loop infinito con salida forzada: en nuestro caso sale cuando el índice i alcanza los 100, pero en un caso real se podría forzar la salida sólo en caso de error, por ejemplo. Tener en cuenta que el thread-loop utiliza una sleep de 10 ms (usando usleep()): ¡intentad olvidar de poner la sleep en un thread-loop realmente infinito y ya veréis los saltos de alegría que hará la CPU del vuestro PC!

Como se puede ver, el tipo tdata contiene un índice típico del thread (en nuestro caso es 0 o 1) y los pointer a los dos datos comunes (locales al main()) que son comdata y lock. Entonces ¿qué hace el thread-loop? Puesto que es un ejemplo simple, sólo incrementa el dato comune comdata inicializado en el main() y lo hace de forma síncronizada utilizando pthread_mutex_lock() y pthread_mutex_unlock() sobre el mutex común lock: esto sirve para evitar que los dos thread accedan al mismo tiempo a comdata.

Compilando con GCC en una máquina Linux (por supuesto) y ejecutando, el resultado es:

aldo@ao-linux-nb:~/blogtest$ gcc thread.c -o thread -pthread
aldo@ao-linux-nb:~/blogtest$ ./thread 
thread 0 comenzado
thread 1 comenzado
thread 1 acabado
thread 0 acabado
./thread: thread acabados: comdata=200

Que es el resultado esperado. En el próximo post hablaremos de una interfaz alternativa a los POSIX Threads. Y, como siempre, os recomiendo que no contengáis la respiración esperando...

¡Hasta el próximo post!

P.D.
Como bien sabéis, este es un blog de programación con un alma cinéfila, así que os comento (con gran tristeza) que el mes pasado nos ha dejado un gran maestro. D.E.P., George.

sábado, 8 de julio de 2017

El bueno, el feo y el VLA
cómo usar los Variable Length Arrays en C - pt.3

Aquí estamos, y, como prometido, este mes vamos a hablar de un pariente cercano de los VLAs, o sea de la función alloca()... ¿será un bueno, un feo o un malo?
¡hola, soy el spoiler de este post!
Entonces, he añadido código en el programa de test para probar la alloca(). Y, para terminar a lo grande, he añadido código para probar la malloc() del C++, o sea la new (después del problemático test de std::vector del último post era correcto acabar el argumento con algo con mejore prestaciones, para que no se diga que tengo algo en contra del C++...). Así que vamos a utilizar programa C++ del post pasado (ya que era prácticamente idéntico a la versión C): os enseño otra vez el main() y las dos funciones de test añadidas (para reconstruir el programa completo es suficiente consultar los dos post anteriores y hacer un poco cut-and-paste). ¡Vamos con el código!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE  1000000

// variable dummy con el fin de evitar el vaciado total de las funciones usando G++ -O2
int avoid_optimization;

// prototipos locales
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void testAllocaVLA(int size);
void testVectorVLA(int size);
void testNewVLA(int size);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// función main()
int main(int argc, char* argv[])
{
    // test argumentos
    if (argc != 2) {
        // error args
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // extrae iteraciones
    int iterations = atoi(argv[1]);

    // ejecuta test
    runTest(iterations, &testVLA, MYSIZE, "testVLA");
    runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
    runTest(iterations, &testStackFLA, 0, "testStackFLA");
    runTest(iterations, &testHeapFLA, 0, "testHeapFLA");
    runTest(iterations, &testAllocaVLA, MYSIZE, "testAllocaVLA");
    runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");
    runTest(iterations, &testNewVLA, MYSIZE, "testNewVLA");

    // sale
    return EXIT_SUCCESS;
}

// función testAllocaVLA()
void testAllocaVLA(
    int size)       // size para alloca()
{
    int *allocavla = (int*)alloca(size * sizeof(int));

    // loop de test
    for (int i = 0; i < size; i++)
        allocavla[i] = i;

    // instrucción con el fin de evitar el vaciado total de las funciones usando G++ -O2
    avoid_optimization = allocavla[size / 2];
}

// función testNewVLA()
void testNewVLA(
    int size)       // size para new
{
    int *newvla = new int[size];

    // loop de test
    for (int i = 0; i < size; i++)
        newvla[i] = i;

    // instrucción con el fin de evitar el vaciado total de las funciones usando G++ -O2
    avoid_optimization = newvla[size / 2];

    delete[] newvla;
}
Como se puede ver, las dos funciones añadidas están perfectamente alineadas estilísticamente con las demás que ya había propuesto y son, como siempre, hiper-comentadas, así que ni siquiera tengo que detenerme en largas explicaciones. ¿Y los resultados de las pruebas? ¡Vamos a verlos!
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tiempo transcurrido: 4.318492 segundos
testMallocVLA -  Tiempo transcurrido: 3.676805 segundos
testStackFLA  -  Tiempo transcurrido: 4.339859 segundos
testHeapFLA   -  Tiempo transcurrido: 4.340040 segundos
testAllocaVLA -  Tiempo transcurrido: 3.678644 segundos
testVectorVLA -  Tiempo transcurrido: 10.934088 segundos
testNewVLA    -  Tiempo transcurrido: 3.679624 segundos
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tiempo transcurrido: 0.746956 segundos
testMallocVLA -  Tiempo transcurrido: 0.697261 segundos
testStackFLA  -  Tiempo transcurrido: 0.696310 segundos
testHeapFLA   -  Tiempo transcurrido: 0.700047 segundos
testAllocaVLA -  Tiempo transcurrido: 0.691677 segundos
testVectorVLA -  Tiempo transcurrido: 1.384563 segundos
testNewVLA    -  Tiempo transcurrido: 0.695037 segundos
Entonces, ¿qué se puede decir? Los resultados de los test de los post anteriores ya los hemos ampliamente comentados, por lo que ahora sólo podemos añadir que: alloca() es muy rápida, ya que es, en práctica, una malloc() en el stack (y utilizándola de manera más apropriada, podría/debería ser la más rápida del grupo). ¿Y la new? Bueno, se comporta (como se esperaba) muy bien, también porque, casi siempre, la new internamente utiliza la malloc().

De acuerdo, la alloca() es rápida, pero lo es (sólo un poco menos) también un VLA, y esto no le ha salvado de ser elegido como el malo de la película. Así que vamos a hacer de nuevo una lista de pros y contras, y a ver cuál es el lado más pesado. Veamos primero los pros:
  1. la alloca() es muy rápida, ya que usa el stack en lugar del heap.
  2. la alloca() es fácil de usar, es una malloc() sin free(). La variable alocada tiene un scope a nivel de función, entonces sigue siendo válida hasta que la función retorna al caller, exactamente como cualquier variable automática local (también un VLA funciona más o menos así, pero su scope es a nivel de bloque, no de función, y esto es, probablemente, un punto a favor de los VLAs).
  3. para la razón explicada en la sección 2, la alloca() no deja residuos de memoria en caso de errores graves en las actividades de una función (con malloc() + free() no es tan fácil lograr esto). Y si estás acostumbrado a utilizar cositas como longjmp() las ventajas en este sentido son grandes.
  4. debido a su aplicación interna (sin entrar en más detalles) la alloca() no provoca la fragmentación de memoria. 
    ¡Uh, qué bien! ¿Y los contras?
    1. la gestión de errores es problemática, porque no hay forma de saber si la alloca() ha alocado bien o ha causado un stack overflow (en este caso provoca efectos similares a los de un error para recursión infinita)... uh, esto es exactamente el mismo problema de los VLAs.
    2. la alloca() no es muy portable, ya que no es una función estándar y su funcionamiento/presencia depende en gran medida del compilador en uso.
    3. la alloca() es error prone (parte 1): hay que utilizarla con cuidado, ya que induce, por lo general, a errores cómo utilizar la variable asignada cuando ya no es válida (pasarla con un return o insertarla en una estructura de datos externa a la función, por ejemplo)... pero nosotros somos buenos programadores y este punto no nos da miedo, ¿no?
    4. la alloca() es error prone (parte  2): hay problemas aún más sutiles que considerar en el uso, por ejemplo puede ser MUY peligroso poner una alloca() dentro de un bucle o de una función recursiva (¡pobre stack!) o en una función inline (que utiliza el stack de manera que choca un poco con la forma de utilizar el stack de la alloca()) ... pero nosotros somos buenos programadores y este punto no nos da miedo, ¿no?
    5. la alloca() es error prone (parte 3): alloca() utiliza el stack, que normalmente está limitado con respecto al heap (especialmente en entornos embedded que son muy frecuentados por los programadores C...). Así agotar el stack y causar un stack overflow es fácil (y difícil de controlar, véase el punto 1)... pero nosotros somos buenos programadores y este punto no nos da miedo, ¿no?
      Vale, ¿las conclusiones? Habría motivos para declarar la alloca() como otro malo (la misma suerte que el VLA), pero teniendo en cuenta las importantes ventajas y, sobre todo, porque hoy estoy de buen humor, le declararemos solo como feo (¿habéis visto el spoiler en la imagen arriba?). De todos modos utilizar la alloca() con mucho cuidado, ¡hombre prevenido vale por dos!

      ¡Hasta el próximo post!