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, 7 de octubre de 2017

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

Con este post cerramos (a lo grande, espero) la mini-serie sobre los thread (Dead Ringers para los amigos).
...exactamente la imagen que te esperas en un blog de programación...
Después de los ejemplos básicos de las dos primeras partes de la serie (¿que acabáis de leer otra vez, verdad? aquí y aquí), es una buena idea enseñar un ejemplo real de una de las muchas aplicaciones que pueden tener los thread. Y entre las muchas, he elegido una que me parece interesante, es decir, un Socket Server multithread, donde cada conexión con un Client remoto se maneja con un thread separado. Una recomendación: antes de seguir, deberíais leer uno de mis antiguos post, o sea: El Server oscuro: la leyenda renace, que es una introducción ideal al tema actual, ya que describe (y bien, espero) la funcionalidad y el código de un Socket Server single-thread. Por cierto, como se verá en breve, el nuevo código que voy a mostrar está estrechamente relacionado con el que se muestraba en el antiguo post.

Y ahora vamos al grano, ¡vamos con el código!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>

#define BACKLOG   10      // para listen()
#define MYBUFSIZE 1024

// prototipos locales
void *connHandler(void *conn_sock);

int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 2) {
        // error args
        printf("%s: numero argumentos equivocado\n", argv[0]);
        printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // crea un socket
    int my_socket;
    if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        // errore socket()
        printf("%s: could not create socket (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // prepara la struct sockaddr_in para este server
    struct sockaddr_in server;          // (local) server socket info
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(atoi(argv[1]));

    // bind informaciones del server al socket
    if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) {
        // error bind()
        printf("%s: bind failed (%s)", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // start escucha con una cola de max BACKLOG conexiones
    if (listen(my_socket, BACKLOG) == -1) {
        // error listen()
        printf("%s: listen failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // acepta conexiones de un client entrante
    printf("%s: espera conexiones entrantes...\n", argv[0]);
    pthread_t thread_id;
    socklen_t socksize = sizeof(struct sockaddr_in);
    struct sockaddr_in client;          // (remote) client socket info
    int client_sock;
    while ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) != -1) {
        printf("%s: conexion aceptada\n", argv[0]);
        if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) {
            // error pthread_create()
            printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno));
            return EXIT_FAILURE;
        }
    }

    // error accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}

// thread function para gestión conexiones
void *connHandler(void *conn_sock)
{
    // extrae el client socket del argumento
    int client_sock = *(int*)conn_sock;

    // loop di recepción mensajes del client
    int read_size;
    char client_msg[MYBUFSIZE];
    while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) {
        // send mensaje de retorno al client
        printf("%s: recibido mensaje del sock %d: %s\n", __func__, client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "me has escrito: %s", client_msg);
        send(client_sock, server_msg, strlen(server_msg), 0);

        // clear buffer
        memset(client_msg, 0, MYBUFSIZE);
    }

    // loop terminado: test motivo
    if (read_size == -1) {
        // error recv()
        printf("%s: recv failed\n", __func__);
    }
    else {
        // read_size == 0: el client se ha desconectado
        printf("%s: client disconnected\n", __func__);
    }

    return NULL;
}

Bueno, no vamos a contar otra vez cómo funciona un Socket Server (ya hecho en el antiguo post, releerlo, please), pero vamos a centrarnos en las diferencias entre el codigo single-thread y el multithread: seguramente habeis notado que son prácticamente idénticos hasta la fase de listen(), e, incluso después, las diferencias son mínimas: la fase de accept() está ahora en un loop y se crea un nuevo thread para cada conexión aceptada (de un Client remoto). ¿Y qué hace el thread? Ejecuta la función connHandler() que contiene, por cierto, el loop de recv() que en el código antiguo se ejecutava inmediatamente después de la fase de accept(). tambien el siguiente test del motivo de salida (antes de tiempo) del loop está contenido en connHandler(), y enseña la señal de error correcta (recv() error o client disconnected, dependiendo del código devuelto por la recv()).

¿Qué añadir? Simple y super-funcional: ¡un Socket Server multithread con cuatro líneas de código! Por supuesto, la sintaxis de creación de los thread  y la ejecución de la start_routine del mismo son idénticas a las descritas aquí. Para probar el nuestro Socket Server, también es necesario compilar un Socket Client (por supuesto el que se describe en mi antiguo post El Client oscuro: la leyenda renace) y ejecutar, por ejemplo, una instancia del Socket Server y dos instancias del Socket Client (en tres terminales diferentes de la misma máquina, o en tres máquinas diferentes). Ejecutando en mi máquina (Linux, por supuesto) en tres terminales, el resultado es el siguiente:
 
En la terminal 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999
./sockserver-mt: espera conexiones entrantes...
./sockserver-mt: conexión aceptada
./sockserver-mt: conexión aceptada
connHandler: recibido mensaje del sock 4: pippo
connHandler: recibido mensaje del sock 5: pluto
connHandler: client disconnected
connHandler: client disconnected

En la terminal 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Escribe un mensaje para el Server remoto: pippo
./sockclient: Server reply: me has escrito: pippo
Escribe un mensaje para el Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

En la terminal 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Escribe un mensaje para el Server remoto: pluto
./sockclient: Server reply: me has escrito: pluto
Escribe un mensaje para el Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

notar que cuando uno de los Client sale (con un CTRL-C, por ejemplo) el Server se da cuenta y enseña, como era de esperar, client disconnected... perfecto.

Ok, hemos acabado con los
thread. Ahora intentaré pensar en algún nuevo tema interesante para el próximo post. Como siempre os recomiendo de no aguantar la respiración en la espera...

¡Hasta el próximo post!

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.