Los Threads como los procesos son mecanismos que permiten a un programa realizar mas de una accion a la vez. Como sucede con los procesos, los Threads al igual que los procesos parecen correr al mismo tiempo; El kernel de Linux los organiza de forma asincronica, interrumpiendo cada Thread durante un tiempo hasta otro tiempo, dando asi a los demas la oportunidad de ejecutarse.
Los Threads existen en conjunto con los procesos, son pequeñas porciones de ejecucion del proceso. Cuando ejecutamos un programa este crea un proceso y en ese proceso se crea un Thread simple que ejecuta el programa de forma secuencial. Ese Thread puede crear threads adicionales y dichos threads ejecutan el mismo programa en el mismo proceso. pero cada thread puede estar ejecutando una parte diferente de programa en cualquier momento dado.
Hay que tomar en cuenta algunas cosas en los Threads:
- Existen con el proceso y utilizan los recursos del proceso.
- Tienen su propio flujo independiente de control durante el tiempo que el proceso dueño del thread exista.
- Duplica solamente los recursos esenciales necesarios para ser independientemente planificable.
- Puede compartir recursos del proceso con otros Threads que tambien actuan de manera independiente o dependiente.
- El thread es terminado si el proceso dueño del Thread muere.
- Es ligero debido a que la mayoria del consumo se ha conseguido mediantre la creacion de su proceso.
Los Threads con el mismo proceso comparten recursos debido a las siguientes caracteristicas:
- Los cambios realizados por un Thread a un recurso compartido pueden ser visto por todos los demas threads.
- Dos punteros como el mismo valor apuntan a los mismos datos.
- La lectura y escritura hacia la misma direccion de memoria es posible y se necesita de forma explicita que el programador realice la sincronizacion.
Creacion de Threads.
Inicialmente el main() comprende un unico thread por defecto, los demas threads deberan ser creados por el programador. Cada threads es identificado por un thread ID, cuando nos referimos a threads ID en C o C++ utilizaremos el tipo ppthread_t.
A su creacion cada Thread debera ejecutar una funcion Thread. Esta es una funcion ordinaria y contiene el codigo que el Thread deberia ejecutar. Cuando la funcion retorne el thread terminara. En GNU/Linux las funciones threads toman un simple parametro de tipo void*, y se tiene un valor de retorno de tipo void*.
La funcion encargada de crear un nuevo thread es la funcion pthread_create
Código
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
donde:
pthread_t *thread es un unico identificador para el nuevo thread devuelto por la subrutina
const pthread_attr_t *attr es un puntero a un objeto de atributos para threads. Este objeto controlara detalles sobre como el thread interactuara con el resto del programa. Si se pasa NULL como atributo para el thread, el thread sera creado con los atributos por defectos.
void*(*start_routine)(void*) es un puntero a la funcion del thread, la cual es un puntero a una funcion del tipo void* (*) void(*).
void *arg es un argumento para el thread del tipo void*, lo que sea que pases sera pasado como argumento a la funcion del thread cuando el thread empiece a ejecutarse.
Una llamada a pthread_create retorna el control al thread principal inmediatamente para que este continue su ejecucion, mientras tanto el nuevo thread inicia su ejecucion. Linux planifica ambos threads de forma asincronica por lo tanto no se tiene un control de cual thread ira avanzando mas rapido que el otro por lo tanto para trabajar se necesitara una sincronizacion que veremos mas adelante.
Veamos un pequeño ejemplo pero primero debemos tomar algo en cuenta; Se debe enlazar la libreria pthread al programa cada vez que se utilizen threads, esto lo realizaremos añadiendo '-lpthread' al momento de enlazar.
Código
#include <pthread.h> #include <stdio.h> void* MundoThread(void *ptr) { int j=0; } int main() { int i=0; pthread_t thread_id; //Con esto crearemos una variable para obtener el thread_id pthread_create(&thread_id,NULL,&MundoThread,NULL);//Creamos el Thread return 0; }
Pasando Datos al Thread.
Pasar datos a traves del argumento nos da una manera sencilla de proveer al thread de los datos que necesita para realizar una correcta ejecucion. Como se pudieron dar cuenta la variable que recibe los argumentos es de tipo void* por lo cual no se podria pasar una gran cantidad de datos directamente a traves del argumento, por lo tanto utilizaremos el argumento del thread para pasar un puntero a una estructura o a una cadena de caracteres. Es recomendable y comunmente utilizado el crear una estructura para cada thread que se quiera ejecutar la cual contenga los datos que dicho thread necesitara.
Código
#include <pthread.h> #include <stdio.h> struct thread_arg { int ini; int fin; int par; }; void* Count(void* argument) { struct thread_arg* arg = (struct thread_arg*) argument; for(arg->ini;arg->ini<=arg->fin;arg->ini++) { if(arg->par==1) { } else { } } } int main() { pthread_t thread_id1, thread_id2; struct thread_arg thread1; struct thread_arg thread2; int junk; thread1.ini=0; thread1.fin=10000; thread1.par=1; thread2=thread1; thread2.par=0; pthread_create(&thread_id1,NULL, &Count,&thread1); pthread_create(&thread_id2,NULL, &Count,&thread2); return 0; }
Si se fijan este codigo deberia de funcionar, pero no muestra nada en pantalla. Esto se debe a que cuando el proceso termina los threads terminan sin importar si completaron o no su funcion. Para este caso pasaremos al proximo tema.
Esperando Threads
En el codigo anterior vimos que aunque deberia de funcionar no muestra nada en pantalla debido a que el proceso termina antes de que estos lleguen a ejecutarse. Una solucion a esto es forzar el Thread principal (main()) a esperar a que los demas Threads terminen. Para estos casos tenemos la funcion pthread_join()
Código
int pthread_join(pthread_t thread, void **value_ptr);
pthread_t thread es el thread por el cual esperaremos.
void **value_ptr es un puntero a una variable tipo void* que recibira el valor de retorno del thread. En caso de que no se quiera saber el valor de retorno del thread se puede pasar el valor NULL al segundo parametro.
Ahora veremos el ejemplo pasado de una forma que mostrara la salida de los threads por pantalla y luego terminara el proceso.
Código
#include <pthread.h> #include <stdio.h> struct thread_arg { int ini; int fin; int par; }; void* Count(void* argument) { struct thread_arg* arg = (struct thread_arg*) argument; for(arg->ini;arg->ini<=arg->fin;arg->ini++) { if(arg->par==1) { } else { } } } int main() { pthread_t thread_id1, thread_id2; struct thread_arg thread1; struct thread_arg thread2; int junk; thread1.ini=0; thread1.fin=10000; thread1.par=1; thread2=thread1; thread2.par=0; pthread_create(&thread_id1,NULL, &Count,&thread1); pthread_create(&thread_id2,NULL, &Count,&thread2); pthread_join(thread_id1,NULL); pthread_join(thread_id2,NULL); return 0; }
Valor de Retorno en un Thread
En caso de que el segundo argumento no sea NULL estamos requiriendo que nos sean devuelto los valores de retorno. Este valor de retorno sera colocado en la direccion pasada como argumento. Hay que tomar en cuenta que el valor de retorno es como el argumento en pthread_create(), es de tipo void*.
Código
#include <pthread.h> #include <stdio.h> void* Euler(void *ptr) { int j=0; int a=0; int b=*((int*)ptr); for(j;j<=b;j++)a+=j; return (void*)a; } int main() { int i=0, num; pthread_t thread_id; pthread_create(&thread_id,NULL,&Euler,&num); pthread_join(thread_id,(void*)&i); return 0; }
Atributos en Threads
Los Atributos en los Threads nos dan la posibilidad de optimizar el funcionamiento de un thread en especifico. La funcion pthread_create acepta como segundo parametro un puntero a un objeto de atributos de thread. En caso de que se pase NULL el thread se iniciara con los atributos por defecto. Para modificar los atributos de un thread simplemente hay que realizar algunos pasos.
1) Creamos una variable del tipo pthread_attr_t.
2) Llamamos la funcion pthread_attr_init() pasando como parametro la direccion de memoria de la variable del tipo. pthread_attr_t.
3) Modificamos el objeto que contiene los atributos para contener los atributos decididos.
4) Pasamos a pthread_create() la direccion de memoria del objeto con los atributos en el segundo parametro.
5) Llamamos pthread_attr_destroy() para liberar el objeto de los atributos.
Para modificar los objetos existen unas funciones especificas pthread_attr_ las cuales los invitare a que lean sobre el tema debido a que posiblemente a muchos no les sirva de mucho darle importancia sobre los atributos a los threads
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setaffinity_np.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setdetachstate.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setguardsize.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setinheritsched.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setschedparam.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setschedpolicy.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setscope.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setstack.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setstackaddr.3.html
http://www.kernel.org/doc/man-pages/online/pages/man3/pthread_attr_setstacksize.3.html
Sincronizacion y Zonas Criticas.
Cuando programamos utilizando threads debemos tener un especial cuidado debido a que no sabemos como el S.O. ha planificado la ejecucion, por lo tanto un thread puede detenerse para dar paso a otro inmediatamente inicie su ejecucion como puede durar una gran cantidad de tiempo.
La causa mas probable de fallo cuando se trabaja con threads es que varios threads quieran utilizar el mismo recurso a la vez. Por ejemplo pensemos en un programa que tiene dos Threads realizando una lista de tareas hasta que la terminen todas. Llega el caso de que ambos procesos terminen sus respectivas tareas y quede solo una; El primer thread llega y toma la tarea, pero antes de marcarla como que la tomo debido a la planificacion del S.O. dicho thread se pausa y entra en funcionamiento el segundo thread. Aqui hay un problema debido a que tendremos varios threads realizando una tarea que se supone que deberia realizarse una sola vez. Para estos casos surgieron los Mutex, esto daba la posibilidad de que solo un thread a la vez pudiese acceder a la lista y mientras ese thread estuviese utilizandola el otro thread no tendria acceso hasta que el thread que accedio primero al recurso tome lo que necesite.
Mutex
El Sistema Operativo nos da la posibilidad de utilizar Mutex como una forma de sincronizacion para evitar el tipo de problema explicado en el caso anterior. como su nombre lo expresa, un Mutex (Mutual Exclusion Locks) como su nombre lo indica son cerraduras especiales las cuales un thread a la vez puede cerrar. Cuando un thread bloquea un mutex y luego otro thread trata de bloquearlo, el segundo thread se queda en espera o es bloqueado, con esto el Sistema Operativo evita que ocurra que dos threads a la vez bloqueen el mismo Mutex y accedan a los mismos recursos.
Para crear un mutex crearemos una variable del tipo pthread_mutex_t y pasaremos un puntero a la funcion pthread_mutex_init() como primer parametro, el segundo parametro sera un puntero a un objeto de atributos de un mutex el cual especificara los aatributos del mutex. Si el segundo parametro es NULL el mutex iniciara con los atributos por defecto. Una variable para un mutex puede ser inicializada una y solo una vez. por cierto existe una forma mas facil de inicializar un mutex y es asignandole el valor especial PTHREAD_MUTEX_INITIALIZER.
Código
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mas info: http://opengroup.org/onlinepubs/007908775/xsh/pthread_mutex_init.html
Un thread puede tratar de bloquear un Mutex llamando la funcion pthread_mutex_lock() en el. En caso de que el Mutex este desbloqueado la funcion bloqueara el Mutex y retornara inmediatamente, pero si esta bloqueado paralizara la ejecucion y retornara cuando el Mutex sea liberado por el otro thread. En caso de que varios threads traten de bloquear un thread ya bloqueado, la ejecucion de todos cesara y cuando el thread que bloqueo el Mutex libere otro Thread lo bloqueara sin un orden predecible especifico. La funcion pthread_mutex_unlock() desbloquea el Mutex y deberia ser llamada por el thread que bloqueo el Mutex.
Código
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
mas info:http://opengroup.org/onlinepubs/007908775/xsh/pthread_mutex_lock.html
Código
#include <pthread.h> #include <stdio.h> pthread_mutex_t mutex[4]; int var[4]={0,0,0,0}; pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER; void *Mult(void *arg) { int k = *((int*)arg); pthread_mutex_lock(&mutex[k]); k++; k*=250; int j = k-249; pthread_mutex_unlock(&lock); for(j;j<=k;j++) { if(j%3==0||j%5==0)var[(k/250)-1]+=j; } pthread_mutex_unlock(&mutex[(k/250)-1]); } int main() { int i; pthread_t threadid[4]; for(i=0;i<4;i++) { pthread_mutex_init(&mutex[i],NULL); pthread_mutex_lock(&lock); pthread_create(&threadid[i],NULL,&Mult,&i); pthread_mutex_lock(&lock); pthread_mutex_unlock(&lock); } for(i=0;i<4;i++)pthread_mutex_lock(&mutex[i]); return 0; }
Semaphores
Los Semaphores al igual que los Mutex son mecanismos para la sincronizacion de threads. Un semaphore es un contador que puede ser utilizado para sincronizar multiples threads, para que multiples threads puedan acceder al mismo recurso (dependiendo de la cantidad de threads admitidos para trabajar a la vez con un recurso).
Un Semaphore es representado por una variable del tipo sem_t y antes de utilizarlos deben de ser inicializados de forma semejante a los mutex, con la funcion sem_init() cuyo primer parametro es un puntero a una variable del tipoo sem_t, el segundo parametro deberia ser cero y el tercer parametro sera el valor inicial del semaphore. Si en algun momento necesitamos saber cual es el valor del Semafore podemos conseguirlos utilizando la funcion sem_getvalue() la cual recibe como primer parametro un puntero a una variable del tipo sem_t inicializada y como segundo parametro un puntero a una variable tipo int para obtener el valor de retorno. En caso de que no se necesite un semaphore es bueno destruirlo con sem_destroy() el cual recibe un puntero a un semaphore.
Código
mas info:[urlhttp://opengroup.org/onlinepubs/007908799/xsh/sem_init.html[/url]
int sem_init(sem_t *sem, int pshared, unsigned int value);
Código
mas info:http://opengroup.org/onlinepubs/007908799/xsh/sem_getvalue.html
int sem_getvalue(sem_t *sem, int *sval);
Código
mas info: http://opengroup.org/onlinepubs/007908799/xsh/sem_destroy.html
int sem_destroy(sem_t *sem);
Cada Semaphore es un contador de enteros positivos y soportan basicamente dos operaciones.
wait: Disminuye el valor del Semaphore en 1, Si el valor del semaphore a la hora de realizar la operacion wait es cero, la funcion queda bloqueada hasta que el valor del semaphore sea positivo. La funcion especifica para realizar un wait en un semaphore es sem_wait() para un wait bloqueante y sem_trywait() para un wait no bloqueante. Ambas funciones reciben como parametro un puntero a una variable del tipo sem_t
post: Incrementa el valor del Semaphore en 1, Si el Semaphore previamente estaba en cero y otro thread estaba bloqueado esperando para realizar un wait en el semaphore, este al realizarse el post se desbloqueara y ejecutara el wait sobre el semaphore. La funcion para realizar un post es sem_post() y al igual que sem_wait() y sem_trywait() recibe un puntero a una variable del tipo sem_t.
Código
mas info:http://opengroup.org/onlinepubs/007908799/xsh/sem_wait.html
int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem);
Código
mas info:http://opengroup.org/onlinepubs/007908799/xsh/sem_post.html
int sem_post(sem_t *sem);
En el ejemplo supongamos que tenemos una piramide de numeros y quisieramos obtener obtener la mayor posible suma de la piramide siguiendo un camino. La piramide esta organizada de la siguiente forma:
Código:
1
8 3
5 6 8
Código
#include <pthread.h> #include <semaphore.h> #include <stdio.h> int piramid[3][3]={{1,0,0},{8,3,0},{5,6,8}}; pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; sem_t sem1, sem21, sem22, sem31, sem32, sem33, semmaster; void* Sum(void* argument) { int var=0; sem_wait(&sem1); pthread_mutex_unlock(&mutex); var+=piramid[0][0]; if(sem_trywait(&sem21)!=-1) { var+=piramid[1][0]; if(sem_trywait(&sem31)!=-1) { var+=piramid[2][0]; sem_post(&semmaster); return (void*)var; } else if(sem_trywait(&sem32)!=-1) { var+=piramid[2][1]; sem_post(&semmaster); return (void*)var; } } else if(sem_trywait(&sem22)!=-1) { var+=piramid[1][1]; if(sem_trywait(&sem32)!=-1) { var+=piramid[2][1]; sem_post(&semmaster); return (void*)var; } else if(sem_trywait(&sem33)!=-1) { var+=piramid[2][2]; sem_post(&semmaster); return (void*)var; } } } int main() { pthread_t thread_id[4]; int i,res=0,sem=0; sem_init(&sem1,0,4); sem_init(&sem21,0,2); sem_init(&sem22,0,2); sem_init(&sem32,0,2); sem_init(&sem31,0,1);//Este trabajaria como si fuera un Mutex sem_init(&sem33,0,1);//Al igual que este sem_init(&semmaster,0,0); for(i=0;i<4;i++) { pthread_mutex_lock(&mutex); pthread_create(&thread_id[i],NULL, &Sum,NULL); } while(sem!=4)sem_getvalue(&semmaster,&sem); for(i=0;i<4;i++) { pthread_join(thread_id[i],(void*)&sem); if(sem>res)res=sem; } return 0; }