Introducción

Varios miembros del foro me han estado pidiendo este artículo desde hace ya algún tiempo, pero por exámenes y estudios no he podido escribirlo.

Lo primero que quiero indicar es que lo que conozco de threads lo he aprendido documentándome por mi cuenta, y mis conocimientos están lejos de ser completos, pero he usado los threads en varias aplicaciones y he aprendido lo suficiente como para explicar lo básico que todo programador debería saber.

Este artículo está pensado para personas con un nivel medio de programación en C o C++, aunque recomiendo que se domine especialmente el C por motivos de pointers y casts a la hora de pasar variables a los threads.

Trataremos la creación y destrucción controlada de distintos threads, el uso de semáforos y variables condicionales para acceder a memoria compartida, y por ultimo otros métodos útiles de comunicación entre threads y procesos.

Si alguien tiene alguna duda o encuentra algún error que me avise en el foro mediante un nuevo tema o un mensaje privado (mi usuario es nake).

¡Espero que sea útil el artículo!


Configuración del compilador y linker

Lo más importante antes de empezar a escribir código es saber qué includes tenemos que añadir y qué librerías al linker.

En el caso de la librería pthreads, tan solo hace falta incluir el header pthread.h
#include ~lt~pthread.h~gt~


Y a la hora de linkear hay que añadir la librería libpthread

Por ejemplo:
gcc -lpthread -o foo.exe -c bar.c


En los distintos IDE de programación esto se hará de distintas formas, normalmente en algún tipo de ventana de configuración del proyecto o similar. Asumo que el lector sabe añadir librerías a su proyecto desde su IDE preferido.

Crear un thread

En cualquier punto del programa (incluso en algún thread) se pueden crear nuevos threads para el proceso. Es decir, el proceso inicial del programa puede crear varios threads, y estos a su vez pueden crear otros threads.

Cuando un thread se crea se le asigna una función que deberá ejecutar. Es algo así como la función main() pero para el nuevo thread.
Esta función, al igual que el main() tiene una definición concreta:

void * foo(void * var)


Al crear el thread se ejecutará en paralelo el programa normal y la función que hemos decidido.

Thread simple

Para crear un thread se hace lo siguiente:
void * foo(void * var) // Función que ejecutará el thread
{
   // Código a ejecutar por el/los threads
   
   return NULL;
}

int main(int argc, char *argv[])
{
   pthread_t mythread; // Variable que almacena la información sobre el thread
   pthread_create(&mythread, NULL, foo, NULL); // Creamos el thread
   
   pthread_exit(NULL); // Última función que debe ejecutar el main() siempre
   return 0;
}


La función pthread_create está declarada como:
int pthread_create(pthread_t* tid, const pthread_attr_t * attr, void *(*start) (void *), void *arg);
tid es el thread.
attr son propiedades que veremos más adelante. Se puede pasar NULL para usar las propiedades por defecto.
start es la función que hemos definido como punto de entrada del thread.
arg es la variable que recibirá la función.

Por utilidad la función tiene una variable var donde podemos dar información al thread, y podemos retornar un valor al terminar el thread.
Como los threads pueden utilizarse para muchas situaciones, estas variables se pasan por referencia usando un void*. De esta forma podemos pasar cualquier tipo de variable existente, incluso structs.

Además, la misma función puede ser usada por distintos threads al mismo tiempo (las variables internas de la función se duplican, cada thread almacena sus variables independientes).

Hay que tener cuidado con cómo se pasan las variables ya que la función acepta un void* y no tiene por qué valer siempre el dar la referencia a la variable y ya está.
Si por ejemplo se pasa la dirección de una variable local, el thread podría intentar leer un lugar de memoria en donde existía una variable que ya se ha borrado.

Por ejemplo, pasemos un valor a varios threads:
int lista_ids[10];

void * foo(void * var) // Función que ejecutará el thread
{
   int my_id = *((int*)var);
   printf("Soy el thread %i!\n", my_id);
   
   return NULL;
}

int main(int argc, char *argv[])
{
   int i;
   pthread_t mythread[10];
   
   for(i=0; i~lt~10; i++)
   {
      lista_ids[i] = i+1;
      printf("Creando thread %i\n", i+1);
      pthread_create(&(mythread[i]), NULL, foo, &(lista_ids[i]));
      // Ojo: No vale pasar una variable local, como por ejemplo &i
   }
   
   pthread_exit(NULL);
   return 0;
}


Esto da por ejemplo como salida:
Creando thread 1
Creando thread 1
Soy el thread 1!
Creando thread 2
Creando thread 3
Creando thread 4
Creando thread 5
Creando thread 6
Creando thread 7
Creando thread 8
Soy el thread 3!
Soy el thread 5!
Soy el thread 7!
Creando thread 9
Soy el thread 2!
Soy el thread 4!
Creando thread 10
Soy el thread 6!
Soy el thread 9!
Soy el thread 8!
Soy el thread 10!


Se ejecutan de forma desordenada, ya que están funcionando al mismo tiempo. Cada vez que se ejecuta sale una salida distinta porque el sistema operativo les da paso cuando quiere.

Para evitar problemas he usado una variable global y no leo ni escribo cuando hay varios procesos ejecutándose. (Cada elemento del vector lista_ids se considera una variable independiente). Es decir, primero establezco el valor de la variable y luego dejo que otro thread lea su valor.

Como los procesadores no pueden realmente ejecutar varios trozos de código a la vez lo que hacen es alternarse. Esto es ajeno al proceso y al usuario, lo hace todo el sistema operativo y puede ocurrir en cualquier punto del código. Eso incluye las funciones que se llaman como printf, fwrite, ¡¡e incluso a la lectura y escritura de variables!! Podrías estar escribiendo un float en un thread y antes de terminar de meterlo en la memoria otro thread podría intentar leerlo dando valores corruptos.

Cómo evitarlo lo explicaré más adelante. Por ahora lo mejor es dejar que cada thread use sus variables y no se comuniquen entre ellos.


Threads con propiedades

Bueno, pero, ¿Cuándo termina un thread? ¿Y si necesito esperar a que un thread termine antes de continuar con otro proceso? O ¿Y si necesito que el thread retorne unos datos al terminar?

Hasta ahora el thread se creaba y funcionaba a su aire. Ejecuta su función y termina al finalizar la función inicial o al cerrar el thread inicial (el main() por ejemplo).

Si queremos que el thread retorne un valor nos encontramos con un problema: ¿cómo sabemos en el thread principal que otro thread ha terminado y quiere darnos su return?.

Para solucionarlo se pueden crear los threads en modo "joinable". Es decir, que el thread inicial puede llamar a una función y esperar a que se cierre el thread y obtener al mismo tiempo el valor retornado. Cuándo llamar a esa función es cosa del programador ya que la función bloquea el thread principal hasta que el thread hijo ha terminado.

Obviamente, tan solo se puede llamara a esa función una vez por thread hijo, ya que solo se ejecuta cuando dicho thread termina.


Lo primero es iniciar el thread como "joinable":
void * foo(void * var)
{
   // Trabajamos como siempre y asignamos el resultado a var
   pthread_exit(var);
   /* Lo que pongamos aquí será lo que se retorne, en este caso he
      usado var porque es una variable que seguro que puede acceder
      la función principal. */
}

int main(int argc, char *argv[])
{
   pthread_t mythread;
   pthread_attr_t attr;
   
   void* ret;
   
   // Iniciamos los atributos del thread
   pthread_attr_init(&attr);
   // Lo queremos joinable
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
   
   // Lo creamos ahora con propiedades
   pthread_create(&mythread, &attr, foo, NULL);
   
   // Esperamos a que se cierre el thread
   pthread_join(mythread, &ret);
   pthread_attr_destroy(&attr); // Borramos de la memoria los atributos
   
   pthread_exit(NULL);
   return 0;
}


Lo importante es que no se retorna con un simple return. Hay que usar la función pthread_exit con la variable a retornar tipo void*. Aquí ocurre lo mismo que antes, la dirección de memoria debe ser accesible por el thread que espera que termine. Si se usa una variable local (o un número literal), lo más seguro es que esa variable haya dejado de existir para cuando lo vaya a leer el otro thread. Se pueden usar variables globales o variables creadas con malloc ya que tienen la memoria fija.

Otra cosa importante es que las propiedades de los threads (atributos) se deben inicializar con pthread_attr_init y desinicializar con pthread_attr_destroy. Es algo así como el uso de malloc / free.

Y por último recordar que la función pthread_join bloquea la ejecución hasta que el thread termine. Esta función se puede usar para esperar a que un thread termine incluso si no se va a retornar un valor.


Acceso a memoria compartida

Semaphores

Como hemos comentado anteriormente, los threads dan problemas si se intenta escribir y/o leer variables globales o memoria compartida puesto que puede producirse corrupción de datos si varios threads acceden a la memoria al mismo tiempo.
Para solucionarlo, el primer método consiste en bloquear el acceso a una variable hasta que el thread haya terminado con ella. Este control se llama semáforo, o mutex.

Pongamos un ejemplo:
Básicamente tenemos una variable global un semáforo y varios threads intentando acceder a la variable.
El thread_1 llega y comprueba el semáforo, como no hay nadie leyendo ni escribiendo a la variable, el semáforo le permite acceder. El thread empieza a escribir los datos.
Ahora llega el thread_2, que quiere leer los datos, pero como el thread_1 aún no ha terminado el semáforo bloquea el acceso y el thread_2 se queda parado.
Un poco más tarde el thread_1 termina y desbloquea la variable permitiendo que el thread_2 lea el valor sin problemas.

En código:
pthread_mutex_t mutex; // Global para que todos puedan acceder al mutex

// Variable compartida, volatile porque se escribe y lee desde distintos threads
volatile int baz;

void * write(void * var)
{
   int i;
   for(i=0; i~lt10; i++)
   {
      pthread_mutex_lock(&mutex); // Bloqueamos el acceso
      baz = i*i; // Operación cualquiera de escritura
      pthread_mutex_unlock(&mutex); // Desbloqueamos la variable
   }
   pthread_exit(NULL);
}

void * read(void * var)
{
   int i;
   for(i=0; i~lt10; i++)
   {
      pthread_mutex_lock(&mutex); // Bloqueamos el acceso
      printf("La variable vale: %i\n", baz);
      pthread_mutex_unlock(&mutex); // Desbloqueamos la variable
   }
   pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
   pthread_t thread_1, thread_2;
   pthread_attr_t attr;
   void* ret;
   
   pthread_mutex_init(&mutex, NULL); // Inicializamos el mutex

   pthread_attr_init(&attr);
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
   
   pthread_create(&thread_1, &attr, write, NULL);
   pthread_create(&thread_2, &attr, read, NULL);
   
   // Esperamos a que se cierre el thread
   pthread_join(thread_1, &ret);
   pthread_join(thread_2, &ret);
   pthread_attr_destroy(&attr); // Borramos de la memoria los atributos
   
   
   /* Antes de llamar a esta función TODOS los threads que usen el mutex
      tienen que estar cerrados. */
   pthread_mutex_destroy(&mutex); // Desinicializa el mutex

   pthread_exit(NULL);
   return 0;
}


Como la función es muy rápida lo más probable es que salga solo todo 0 o todo 81, es únicamente por el ejemplo. Lo importante es que no pueden dar errores de escritura/lectura.

Las funciones nuevas son simples:
pthread_mutex_init para iniciar un mutex.
pthread_mutex_destroy para borrarlo de la memoria. Pero atención, NO se puede acceder al mutex desde ningún thread así que lo mejor es asegurarse que todos los threads que lo usen estén cerrados antes de desinicializar el mutex.
pthread_mutex_lock bloquea el mutex o detiene la ejecución del thread hasta que el mutex esté desbloqueado.
pthread_mutex_unlock desbloquea el mutex permitiendo que otros threads accedan a la variable.

Una nota importante es que el mutex realmente no sabe qué variables está controlando, eso es algo que el programador debe de decidir. Por tanto, un único mutex puede controlar varias variables o incluso ninguna. El programador debe tener todo esto en cuenta y ser consistente en sus decisiones para evitar corrupción de memoria y otros fallos.

Variables condicionales

Este tipo de control entre threads se usa principalmente para sincronizar la ejecución de los threads. El sistema es similar al de los mutexs pero orientado al control de ejecución del thread.

Por ejemplo: El thread_1 realiza unos cálculos, mientras que el thread_2 realiza otros cálculos complejos en paralelo que necesitan los datos del thread_1.
Esto se puede hacer creando una variable condicional y bloqueando el thread_2 hasta que el thread_1 haga sus cálculos. Cuando el thread_1 termina le envía una señal al thread_2 diciéndole que puede continuar.

Las variables condicionales se crean prácticamente igual que los mutex:
pthread_cond_t myconvar;
Al igual que los mutex, deben de ser variables globales para permitir que los threads puedan usarlos.

Se inicializan antes de ser usados:
pthread_cond_init (&myconvar, NULL);
Y se desinicializan al terminar con ellos en todos los threads:
pthread_cond_destroy(&myconvar);

La única diferencia es que para usar las variables condicionales también se deben usar los mutex a la vez.
Ejemplo:
__THREAD_A__
pthread_mutex_lock(&mutex); // SIEMPRE debemos de haber bloqueado antes

// Esta función desbloquea automáticamente y espera a myconvar
pthread_cond_wait(&myconvar, &mutex);

/* SIEMPRE debemos desbloquear ya que
   cond_wait bloquea automáticamente al salir. */
pthread_mutex_unlock(&mutex);

__THREAD_B__
pthread_mutex_lock(&mutex);

/* Enviamos la señal que espera THREAD_A (Si hay varios threads esperando
hay que usar thread_cond_broadcast para enviar a todos)*/
pthread_cond_signal(&myconvar);

pthread_mutex_unlock(&mutex);


Lo mejor es no enviar signals si no hay nadie esperando el signal. Se pueden usar variables globales para comprobar si algún thread está esperando una señal o no, aunque en la mayoría de los casos reales el thread que genera la señal ya sabe que hay un thread esperándolo porque si no no estaría realizando los cálculos y enviando el signal. Pero eso es algo que el programador tiene que tener en cuanta.

Sleep

Teóricamente la función sleep(en linux) y Sleep(en windows) deberían de detener únicamente el thread que lo ha ejecutado, pero la experiencia enseña que al menos en Windows esto no es así, y que en vez de detener uno de los threads se detienen todos los threads.

Para solucionar este problema se suele usar la función nanosleep, que nunca da problemas.

El problema es que no es una función tan simple como sleep.
El funcionamiento es el siguiente:
#include 

struct timespec sleepTime;
struct timespec remainingSleepTime;

sleepTime.tv_sec = n_segundos_a_parar;
sleepTime.tv_nsec = n_nanosegundos_a_parar;
nanosleep(&sleepTime,&remainingSleepTime);


remainingSleepTime debería valer siempre 0 excepto en ciertas situaciones como que al programa se le envíen signals externos. En la mayor parte de las situaciones se puede asumir que vale 0.


Enlaces externos y bibliografía

Documentación oficial (con ejemplos) de pthread: https://computing.llnl.gov/tutorials/pthreads/
Comunicación entre procesos: http://linuxgazette.net/104/ramankutty.html
Wikipedia (tiene ejemplos): http://en.wikipedia.org/wiki/POSIX_Threads

Foro de esta web donde preguntar y comentar: http://www.nakerium.com/foro/

Este artículo ha sido creado enteramente por mí (nake) y está publicado bajo licencia Creative Commons.

Licencia de Creative CommonsMultithreading en C/C++ usando Pthreads (Windows y Linux) by nake is licensed under a Creative Commons Reconocimiento-NoComercial-CompartirIgual 3.0 Unported License.