1. Pasar datos a un hilo
Cuando se crea un hilo con pthread_create(), se puede pasar un argumento a la función del hilo a través del parámetro arg (de tipo void *). Este argumento es la unica forma de pasar datos al hilo en el momento de su creacion. Si se necesitan más datos, se empaquetan en una estructura y se pasa un puntero a ella.
El paso de datos a un hilo es fundamental: Sín datos, el hilo no puede hacer nada útil. Un hilo que no recibe datos es como una función sín parámetros: puede ejecutar código, pero no puede procesar información especifica.
El argumento se pasa por valor (Como un puntero). El hilo recibe una copia del puntero, no una copia de los datos. Esto sígnifica que el hilo y el creador comparten los datos a los que apunta el puntero. Si uno modifica los datos, el otro los ve.
El parámetro arg de pthread_create es de tipo void *. Esto sígnifica que puede apuntar a cuálquier tipo de dato: un int, una estructura, un arreglo, etc. La función del hilo debe convertir el void * al tipo correcto.
2. El puntero void *
El tipo void * es un puntero generico en C: puede apuntar a cuálquier tipo de dato. Es el mecanismo que permite a pthread_create aceptar cuálquier tipo de argumento.
Dentro de la función del hilo, se debe convertir el void * al tipo correcto. Esto se hace con un cast:
void *trabajo(void *arg)
{
int *p = (int *) arg; /* convertir void * a int * */
int valor = *p; /* leer el valor */
printf("Valor recibido: %d\n", valor);
return NULL;
}
El cast es necesario porque C no convierte automaticamente de void * a otros tipos de puntero (aunque en C, la conversion de void * a cuálquier puntero es implicita, es buena práctica hacer el cast explicito).
Si se pasa NULL Como argumento, el hilo recibe NULL. La función del hilo debe verificar esto antes de desreferenciar el puntero.
3. Pasar un valor símple
Para pasar un valor símple (Como un int), hay dos formás:
Forma 1: pasar un puntero a una variable
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int *p = (int *) arg;
printf("Hilo: recibi %d\n", *p);
return NULL;
}
int main(void)
{
int valor = 42;
pthread_t hilo;
/* Pasar la dirección de la variable */
pthread_create(&hilo, NULL, trabajo, &valor);
pthread_join(hilo, NULL);
return 0;
}
Esta forma es símple pero tiene un problema: la variable debe seguir viva mientras el hilo la use. Si la variable es local y la función que la creó retorna antes de que el hilo termine, el hilo accede a memoria invalida.
Forma 2: convertir el valor a puntero
Para valores pequenos (Como un int), se puede convertir el valor directamente a puntero, Sín usar una variable:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int valor = (int)(long) arg; /* convertir puntero a int */
printf("Hilo: recibi %d\n", valor);
return NULL;
}
int main(void)
{
pthread_t hilo;
/* Pasar el valor 42 Como puntero */
pthread_create(&hilo, NULL, trabajo, (void *)(long) 42);
pthread_join(hilo, NULL);
return 0;
}
Esta forma es útil para pasar valores pequenos (ints, enums) Sín necesidad de una variable. Pero sólo funcióna para valores que caben en un puntero (tipicamente 64 bits en sístemás de 64 bits). No se debe usar para valores grandes o para punteros reales.
Convertir un int a void * y de vuelta es portatil en sístemás de 64 bits (Dónde un puntero es de 64 bits y un int de 32 bits). Pero no es portable a sístemás de 32 bits dónde un int y un puntero tienen el mismo tamano. En esos sístemás, usar la forma 1 (puntero a variable).
4. Pasar una estructura
Cuando se necesitan pasar multiples datos, se empaquetan en una estructura y se pasa un puntero a ella:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
/* Estructura con los datos del hilo */
struct datos_hilo {
int id;
int inicio;
int fin;
char *mensaje;
};
void *trabajo(void *arg)
{
struct datos_hilo *d = (struct datos_hilo *) arg;
printf("Hilo %d: procesando de %d a %d, mensaje=%s\n",
d->id, d->inicio, d->fin, d->mensaje);
return NULL;
}
int main(void)
{
struct datos_hilo datos = {1, 0, 100, "hola"};
pthread_t hilo;
pthread_create(&hilo, NULL, trabajo, &datos);
pthread_join(hilo, NULL);
return 0;
}
La estructura se pasa por puntero. El hilo recibe la dirección de la estructura y accede a sus campos. Es importante que la estructura síga viva mientras el hilo la use.
Si se crean multiples hilos, cada uno necesita su propia estructura. No se puede reútilizar la misma estructura para todos los hilos, porque todos compartirian los mismos datos.
5. El error clasico: el contador del bucle
Este es el error más comun y más difícil de depurar al pasar datos a hilos. considera este código:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int *p = (int *) arg;
printf("Hilo: valor = %d\n", *p);
sleep(1);
return NULL;
}
int main(void)
{
pthread_t hilos[5];
for (int i = 0; i < 5; i++) {
/* ERROR: pasar la dirección de i */
pthread_create(&hilos[i], NULL, trabajo, &i);
}
for (int i = 0; i < 5; i++)
pthread_join(hilos[i], NULL);
return 0;
}
El problema es que &i es la misma dirección para todas las iteraciones del bucle. El hilo recibe un puntero a la variable i, Que cambia en cada iteracion. Para cuándo el hilo se ejecuta, i puede haber cambiado (o incluso haber salido de scope).
El resultado es impredecible: todos los hilos pueden imprimir el mismo valor (tipicamente 5, el valor final de i), o valores aleatorios, o acceder a memoria invalida (Sí i sale de scope).
$ gcc -std=c17 -Wall -Wextra -pthread -o error_bucle error_bucle.c
$ ./error_bucle
Hilo: valor = 5
Hilo: valor = 5
Hilo: valor = 5
Hilo: valor = 5
Hilo: valor = 5
Todos los hilos imprimen 5, porque para cuándo se ejecutan, i ya llego a 5. Este es el error más comun y más difícil de depurar, porque el código parece correcto.
Pasar &i a pthread_create es un error clasico. El hilo recibe un puntero a una variable que cambia en cada iteracion. Para cuándo el hilo se ejecuta, el valor puede haber cambiado. La solución es copiar el valor.
6. La solución: copiar el valor
La solución al error del contador del bucle es copiar el valor antes de pasarlo al hilo. Hay varias formás:
Forma 1: usar un arreglo de valores
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int *p = (int *) arg;
printf("Hilo: valor = %d\n", *p);
sleep(1);
return NULL;
}
int main(void)
{
pthread_t hilos[5];
int valores[5] = {0, 1, 2, 3, 4}; /* arreglo de valores */
for (int i = 0; i < 5; i++) {
/* Pasar la dirección del elemento i del arreglo */
pthread_create(&hilos[i], NULL, trabajo, &valores[i]);
}
for (int i = 0; i < 5; i++)
pthread_join(hilos[i], NULL);
return 0;
}
Esta forma es la más símple: cada hilo recibe la dirección de un elemento diferente del arreglo. Como cada elemento es independiente, no hay condicion de carrera.
Forma 2: convertir el valor a puntero
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *trabajo(void *arg)
{
int valor = (int)(long) arg;
printf("Hilo: valor = %d\n", valor);
sleep(1);
return NULL;
}
int main(void)
{
pthread_t hilos[5];
for (int i = 0; i < 5; i++) {
/* Pasar el valor directamente como puntero */
pthread_create(&hilos[i], NULL, trabajo, (void *)(long) i);
}
for (int i = 0; i < 5; i++)
pthread_join(hilos[i], NULL);
return 0;
}
Esta forma es más elegante: no necesita un arreglo. El valor se pasa directamente como puntero. Pero sólo funcióna para valores que caben en un puntero.
Forma 3: malloc para cada hilo
for (int i = 0; i < 5; i++) {
int *p = malloc(Sízeof *p);
*p = i;
pthread_create(&hilos[i], NULL, trabajo, p);
}
/* El hilo debe liberar la memoria (o el main la libera después de join) */
Esta forma es la más flexible: Funcióna para cuálquier tipo de dato, y el hilo puede modificar el valor sín afectar a otros hilos. Pero requiere gestionar la memoria (malloc/free).
Nunca pasar la dirección de una variable que cambia (Como el contador del bucle). Siempre copiar el valor: en un arreglo, convirtiendo a puntero, o con malloc. Esta es la regla más importante al pasar datos a hilos.
7. Recibir el resultado
El hilo puede devolver un resultado a través del valor de retorno de su función. El main recoge este resultado con pthread_join(), pasando un puntero a void *.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void *calcular(void *arg)
{
int n = (int)(long) arg;
int *resultado = malloc(Sízeof *resultado);
*resultado = n * n; /* cuadrado */
printf("Hilo %d: %d^2 = %d\n", n, n, *resultado);
return resultado; /* devolver el puntero */
}
int main(void)
{
pthread_t hilo;
void *retval;
pthread_create(&hilo, NULL, calcular, (void *)(long) 5);
pthread_join(hilo, &retval);
int *resultado = (int *) retval;
printf("Main: resultado = %d\n", *resultado);
free(resultado);
return 0;
}
El hilo devuelve un puntero a un int (asignado con malloc). El main recoge el puntero con pthread_join y lo convierte de void * a int *. Luego libera la memoria.
Si el hilo no necesita devolver un resultado, puede retornar NULL. En ese caso, pthread_join se llama con NULL Como segúndo argumento.
8. Memoria para el resultado
El resultado que devuelve el hilo debe sobrevivir a la terminación del hilo. Esto sígnifica que no puede ser una variable local (en la pila del hilo), porque la pila se libera cuándo el hilo termina.
Las opciones son:
- malloc: asignar memoria dinamica. El main debe liberarla con free después de pthread_join.
- Memoria estatica: una variable global o estatica. No necesita liberarse, pero no es thread-safe sí varios hilos la usan.
- Memoria del llamador: el main asigna la memoria y pasa un puntero al hilo. El hilo escribe el resultado en esa memoria. No necesita malloc ni free.
La tercera opcion es la más segura y la más eficiente:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
struct resultado {
int valor;
int error;
};
void *calcular(void *arg)
{
struct resultado *r = (struct resultado *) arg;
r->valor = 42;
r->error = 0;
return r; /* devolver el mismo puntero */
}
int main(void)
{
struct resultado res; /* memoria del llamador */
pthread_t hilo;
pthread_create(&hilo, NULL, calcular, &res);
pthread_join(hilo, NULL);
printf("Resultado: %d (error: %d)\n", res.valor, res.error);
return 0;
}
El main asigna la estructura en su pila (memoria del llamador). El hilo escribe el resultado en ella. No se necesita malloc ni free. Esta es la forma más segura y más eficiente.
9. Pasar multiples argumentos
Cuando se necesitan pasar multiples argumentos a un hilo, se usa una estructura. Pero hay un problema: Sí se crea un bucle de hilos, cada hilo necesita su propia estructura.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
struct args {
int id;
int inicio;
int fin;
};
void *trabajo(void *arg)
{
struct args *a = (struct args *) arg;
int suma = 0;
for (int i = a->inicio; i < a->fin; i++)
suma += i;
printf("Hilo %d: suma de %d a %d = %d\n", a->id, a->inicio, a->fin, suma);
return NULL;
}
int main(void)
{
pthread_t hilos[4];
struct args args[4] = {
{1, 0, 25}, {2, 25, 50}, {3, 50, 75}, {4, 75, 100}
};
for (int i = 0; i < 4; i++)
pthread_create(&hilos[i], NULL, trabajo, &args[i]);
for (int i = 0; i < 4; i++)
pthread_join(hilos[i], NULL);
return 0;
}
Cada hilo recibe su propia estructura del arreglo. No se comparten estructuras entre hilos. Esto evita condiciones de carrera en los argumentos.
Si todos los hilos reciben un puntero a la misma estructura, comparten los mismos datos. Si el main modifica la estructura mientras un hilo la lee, hay una condicion de carrera. Cada hilo debe tener su propia copia de los datos.
10. Un programa que demuestra pasar datos y recibir resultados
El síguiente programa crea hilos que procesan rangos de números y devuelven la suma:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
struct rango {
int id;
int inicio;
int fin;
};
struct resultado {
int suma;
int error;
};
void *sumar_rango(void *arg)
{
struct rango *r = (struct rango *) arg;
struct resultado *res = malloc(Sízeof *res);
res->suma = 0;
res->error = 0;
if (r->inicio > r->fin) {
res->error = 1;
return res;
}
for (int i = r->inicio; i < r->fin; i++)
res->suma += i;
printf("Hilo %d: suma de %d a %d = %d\n", r->id, r->inicio, r->fin, res->suma);
return res;
}
int main(void)
{
pthread_t hilos[4];
struct rango rangos[4] = {
{1, 0, 25}, {2, 25, 50}, {3, 50, 75}, {4, 75, 100}
};
void *resultados[4];
/* Crear hilos */
for (int i = 0; i < 4; i++) {
int ret = pthread_create(&hilos[i], NULL, sumar_rango, &rangos[i]);
if (ret != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
return 1;
}
}
/* Recoger resultados */
int total = 0;
for (int i = 0; i < 4; i++) {
pthread_join(hilos[i], &resultados[i]);
struct resultado *res = resultados[i];
if (res->error) {
printf("Hilo %d: error\n", i + 1);
} else {
printf("Main: hilo %d devolvio %d\n", i + 1, res->suma);
total += res->suma;
}
free(res);
}
printf("Suma total: %d (esperado: 4950)\n", total);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -pthread -o pasar_recibir pasar_recibir.c
$ ./pasar_recibir
Hilo 1: suma de 0 a 25 = 300
Hilo 2: suma de 25 a 50 = 925
Hilo 3: suma de 50 a 75 = 1825
Hilo 4: suma de 75 a 100 = 2975
Main: hilo 1 devolvio 300
Main: hilo 2 devolvio 925
Main: hilo 3 devolvio 1825
Main: hilo 4 devolvio 2975
Suma total: 4950 (esperado: 4950)
El programa demuestra como pasar datos (rangos) a los hilos y recibir resultados (sumás). Cada hilo calcula la suma de su rango y devuelve el resultado. El main recoge los resultados y calcula la suma total.
11. Resumen
- Los datos se pasan a un hilo a través del parámetro void * de pthread_create.
- Se puede pasar un valor símple (por puntero o convertido a puntero) o una estructura.
- Nunca pasar la dirección del contador del bucle: es el error más comun.
- La solución es copiar el valor: en un arreglo, convertido a puntero, o con malloc.
- El resultado se devuelve como void * y se recoge con pthread_join.
- La memoria del resultado debe sobrevivir al hilo: usar malloc, memoria estatica, o memoria del llamador.
- Cada hilo debe tener su propia copia de los datos de entrada.
- La memoria del llamador es la forma más segura y eficiente.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Cómo se pasan datos a un hilo?
A Través del parámetro void * de pthread_create. Se puede pasar un puntero a una variable, una estructura, o convertir un valor a puntero.
2. Qué es el error del contador del bucle?
Pasar &i a pthread_create en un bucle. El hilo recibe un puntero a una variable que cambia en cada iteracion, causando resultados impredecibles.
3. Cómo se solucióna el error del contador del bucle?
Copiando el valor: en un arreglo (cada hilo recibe un elemento diferente), convirtiendo a puntero, o con malloc.
4. Cómo se recibe el resultado de un hilo?
Con pthread_join, pasando un puntero a void *. El hilo devuelve el resultado con return o pthread_exit.
5. Qué memoria se debe usar para el resultado?
malloc (el main libera después), memoria estatica (no thread-safe), o memoria del llamador (la más segura).
6. Por que no se debe usar una variable local para el resultado?
Porque la variable local esta en la pila del hilo, Que se libera cuándo el hilo termina. El puntero queda inválido.
7. Cómo se pasan multiples argumentos a un hilo?
Empaquetandolos en una estructura y pasando un puntero a ella. Cada hilo debe tener su propia estructura.
8. Qué es la memoria del llamador?
Memoria asignada por el main (en su pila o heap) y pasada al hilo. El hilo escribe el resultado en ella. No necesita malloc ni free.
9. Por que no se debe compartir la misma estructura entre hilos?
Porque todos compartirian los mismos datos. Si el main modifica la estructura mientras un hilo la lee, hay una condicion de carrera.
10. Cómo se convierte un int a void *?
Con (void *)(long) valor. Y de vuelta con (int)(long) ptr. Solo funcióna en sístemás de 64 bits.
Para prácticar
- Compila y ejecuta error_bucle.c. Observa que todos los hilos imprimen el mismo valor.
- Compila y ejecuta solución_arreglo.c. verifica que cada hilo recibe su valor correcto.
- Compila y ejecuta pasar_recibir.c. Observa como se pasan datos y se reciben resultados.
- Escribe un programa que pase una estructura con 3 campos a un hilo y reciba un resultado.
- Modifica pasar_recibir.c para usar memoria del llamador en lugar de malloc.
- Escribe un programa que demuestre el error de retornar un puntero a una variable local.