Saltar al contenido
ES EN
Tema: automático

Ejercicio resuelto: 6 hilos POSIX que se identifican en C

Ejercicio de examen resuelto en C: seis hilos POSIX con identificador propio, pthread_create, pthread_join y una salida determinista, con tests.

Actualizado el

Código de Examen C Sistemas operativos Nivel examen Código probado

Código de Examen: Ejercicio resuelto: 6 hilos POSIX que se identifican en C
Sistemas Operativos · Examen parcialEjercicio 2 · 2,5 puntos · 30 min

Un laboratorio de matemática experimental quiere repartir un cálculo entre exactamente 6 hilos POSIX. Cada hilo tiene un identificador lógico del 1 al 6, que es el que debe usar para identificarse. No vale el identificador del sistema que devuelve pthread_self(), porque cambia en cada ejecución.

Cada hilo recibe un número natural n y calcula cuántos pasos necesita la secuencia de Collatz para llegar a 1. Un paso transforma x en x/2 si x es par y en 3x+1 si es impar. Por convenio, para n = 1 el número de pasos es 0.

Entrada (por la entrada estándar): seis enteros separados por espacios o saltos de línea. El primero corresponde al hilo 1, el segundo al hilo 2, y así sucesivamente. Cada valor debe cumplir 1 <= n <= 1000000000. Si hay más de seis números, los sobrantes se ignoran.

Puede haber menos de seis enteros (incluida la entrada vacía), un dato que no sea un entero o un valor fuera de rango. En cualquiera de esos casos el programa no crea ningún hilo y escribe una única línea: Entrada incorrecta.

Salida: seis líneas, ordenadas por identificador, con el formato Hilo I: n=N pasos=P. Después, una línea Maximo: hilo I con P pasos que indica el hilo con más pasos. Si hay empate, gana el identificador menor.

La salida debe ser idéntica en todas las ejecuciones, sea cual sea el orden en que el planificador ejecute los hilos. El programa principal debe esperar a que terminen todos los hilos con pthread_join. Se compila con gcc -std=c17 -O2 -Wall -Wextra -lpthread y no debe producir avisos.

Entrada de ejemplo

27 1 6 7 97 871

Salida esperada

Hilo 1: n=27 pasos=111
Hilo 2: n=1 pasos=0
Hilo 3: n=6 pasos=8
Hilo 4: n=7 pasos=16
Hilo 5: n=97 pasos=118
Hilo 6: n=871 pasos=178
Maximo: hilo 6 con 178 pasos
Creación de los 6 hilos con pthread_create pasando a cada uno un argumento propio que no se comparte ni se sobrescribe
0,75
Espera de todos los hilos con pthread_join antes de leer sus resultados
0,5
Cálculo correcto de los pasos de Collatz en el hilo, usando enteros de 64 bits para los valores intermedios
0,5
Validación de la entrada y salida determinista en el formato exacto, incluido el desempate del máximo
0,75

Pistas

Pista 1 · ¿Qué le paso a cada hilo como argumento?

pthread_create solo admite un void *. Si le pasas &i, la variable del bucle, todos los hilos apuntan a la misma posición de memoria, y esa posición sigue cambiando mientras ellos arrancan. Lo correcto es que cada hilo tenga su propia estructura con su identificador, su dato de entrada y un hueco para su resultado. Por ejemplo, un vector de 6 estructuras declarado en main.

Pista 2 · ¿Quién debe imprimir?

Si cada hilo hace printf al terminar, el orden de las líneas depende del planificador y la salida deja de ser determinista. Hay una alternativa más sencilla que sincronizar los hilos entre sí: el hilo solo calcula y deja el resultado en su estructura. El hilo principal imprime después, recorriendo el vector en orden.

Pista 3 · ¿Cuándo es seguro leer el resultado?

Solo cuando pthread_join ha retornado para ese hilo. Ese retorno garantiza que el hilo ha terminado y que lo que escribió en memoria es visible para el hilo principal. Crea los seis hilos primero y haz los seis join después. Si haces join dentro del mismo bucle de creación, el programa funciona, pero los cálculos quedan en serie.

Solución

Solución explicada

La solución separa tres fases: leer y validar toda la entrada, lanzar los hilos y esperarlos, e imprimir. Toda la comunicación entre el hilo principal y los trabajadores pasa por un vector de estructuras Tarea, una por hilo. Cada hilo escribe solo en su propia estructura, de modo que no hay datos compartidos que proteger con cerrojos.

Si necesitas repasar por qué pasar la dirección de una variable permite que otra función la modifique, consulta el ejercicio sobre paso por valor y por referencia en C. Es exactamente el mecanismo que usa el argumento void * de pthread_create.

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

#define NUM_HILOS 6
#define LIMITE 1000000000LL

typedef struct {
    int id;                 /* identificador logico 1..6 */
    unsigned long long n;   /* dato de entrada */
    int pasos;              /* resultado que escribe el hilo */
} Tarea;

static void *trabajar(void *arg)
{
    Tarea *t = (Tarea *)arg;
    unsigned long long x = t->n;
    int pasos = 0;

    while (x != 1ULL) {
        if (x % 2ULL == 0ULL) {
            x /= 2ULL;
        } else {
            x = 3ULL * x + 1ULL;
        }
        pasos++;
    }
    t->pasos = pasos;   /* solo escribe en su propia estructura */
    return NULL;
}

int main(void)
{
    Tarea tareas[NUM_HILOS];
    pthread_t hilos[NUM_HILOS];

    /* Fase 1: leer y validar toda la entrada antes de crear hilos */
    for (int i = 0; i < NUM_HILOS; i++) {
        long long v;
        if (scanf("%lld", &v) != 1 || v < 1 || v > LIMITE) {
            printf("Entrada incorrecta\n");
            return 0;
        }
        tareas[i].id = i + 1;
        tareas[i].n = (unsigned long long)v;
        tareas[i].pasos = -1;
    }

    /* Fase 2: crear todos los hilos, cada uno con su propia Tarea */
    for (int i = 0; i < NUM_HILOS; i++) {
        int err = pthread_create(&hilos[i], NULL, trabajar, &tareas[i]);
        if (err != 0) {
            fprintf(stderr, "pthread_create fallo (codigo %d)\n", err);
            for (int j = 0; j < i; j++) {
                pthread_join(hilos[j], NULL);
            }
            return 1;
        }
    }

    /* Fase 3: esperar a todos */
    for (int i = 0; i < NUM_HILOS; i++) {
        int err = pthread_join(hilos[i], NULL);
        if (err != 0) {
            fprintf(stderr, "pthread_join fallo (codigo %d)\n", err);
            return 1;
        }
    }

    /* Fase 4: imprimir en orden de identificador */
    int mejor = 0;
    for (int i = 0; i < NUM_HILOS; i++) {
        printf("Hilo %d: n=%llu pasos=%d\n", tareas[i].id, tareas[i].n, tareas[i].pasos);
        if (tareas[i].pasos > tareas[mejor].pasos) {
            mejor = i;
        }
    }
    printf("Maximo: hilo %d con %d pasos\n", tareas[mejor].id, tareas[mejor].pasos);
    return 0;
}

La estructura Tarea. Agrupa lo que un hilo necesita saber y lo que debe devolver: id (su identificador lógico), n (el dato) y pasos (el resultado). En main declaramos Tarea tareas[NUM_HILOS]. Cada hilo recibe la dirección de un elemento distinto, &tareas[i], y por eso ningún hilo ve ni toca los datos de otro.

Validación primero. El primer bucle lee con scanf("%lld", &v) y comprueba tres cosas: que se ha leído un entero, que v >= 1 y que v <= LIMITE. Si algo falla se imprime Entrada incorrecta y se sale sin haber creado ningún hilo. Así no queda ningún hilo a medias que haya que esperar. Los tests «entrada vacía» y «valor cero» cubren este camino. En el segundo, el tercer número es 0 y el programa rechaza la entrada antes de leer los demás.

La función del hilo. trabajar convierte el void * en Tarea * y aplica la regla de Collatz con un unsigned long long. El tipo importa: para valores iniciales de hasta mil millones, los intermedios superan con creces el rango de un int de 32 bits. Con 64 bits sin signo no hay desbordamiento en ese rango. Al terminar, el hilo guarda el contador en t->pasos y devuelve NULL.

Creación y espera. El segundo bucle crea los seis hilos sin esperar a ninguno, y el tercero hace los seis pthread_join. Si pthread_create falla, se esperan los hilos ya creados (índices 0 a i-1) antes de salir. Así no se abandonan hilos en ejecución. Con un solo procesador los hilos no corren en paralelo real, pero el programa es correcto igual: el diseño no depende de cuántos núcleos haya.

Por qué la salida es determinista. Ningún hilo imprime. El orden de las líneas lo fija el último bucle de main, que recorre tareas de 0 a 5. Los valores son correctos porque se leen después de los join, y el retorno de pthread_join garantiza que la escritura de t->pasos ya es visible. El campo se inicializa a -1 como centinela: si alguien olvidara el join, ese valor delataría el error.

El máximo y el desempate. La variable mejor empieza en 0 y solo cambia si un hilo tiene estrictamente más pasos. Por eso, en caso de empate gana el identificador menor. El test «todos iguales», con seis nueves de 19 pasos cada uno, lo comprueba: el resultado es Maximo: hilo 1 con 19 pasos. En el ejemplo, el máximo es el hilo 6 (n = 871, 178 pasos). En el test de límites gana el hilo 3 con 837799 y 524 pasos, mientras que 536870912 = 229 solo necesita 29 divisiones.

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciado27 1 6 7 97 871Hilo 1: n=27 pasos=111 Hilo 2: n=1 pasos=0 Hilo 3: n=6 pasos=8 Hilo 4: n=7 pasos=16 Hilo 5: n=97 pasos=118 Hilo 6: n=871 pasos=178 Maximo: hilo 6 con 178 pasosHilo 1: n=27 pasos=111 Hilo 2: n=1 pasos=0 Hilo 3: n=6 pasos=8 Hilo 4: n=7 pasos=16 Hilo 5: n=97 pasos=118 Hilo 6: n=871 pasos=178 Maximo: hilo 6 con 178 pasosOK
Todos iguales (desempate)9 9 9 9 9 9Hilo 1: n=9 pasos=19 Hilo 2: n=9 pasos=19 Hilo 3: n=9 pasos=19 Hilo 4: n=9 pasos=19 Hilo 5: n=9 pasos=19 Hilo 6: n=9 pasos=19 Maximo: hilo 1 con 19 pasosHilo 1: n=9 pasos=19 Hilo 2: n=9 pasos=19 Hilo 3: n=9 pasos=19 Hilo 4: n=9 pasos=19 Hilo 5: n=9 pasos=19 Hilo 6: n=9 pasos=19 Maximo: hilo 1 con 19 pasosOK
Valores grandes y límites1 536870912 837799 77031 6171 1024Hilo 1: n=1 pasos=0 Hilo 2: n=536870912 pasos=29 Hilo 3: n=837799 pasos=524 Hilo 4: n=77031 pasos=350 Hilo 5: n=6171 pasos=261 Hilo 6: n=1024 pasos=10 Maximo: hilo 3 con 524 pasosHilo 1: n=1 pasos=0 Hilo 2: n=536870912 pasos=29 Hilo 3: n=837799 pasos=524 Hilo 4: n=77031 pasos=350 Hilo 5: n=6171 pasos=261 Hilo 6: n=1024 pasos=10 Maximo: hilo 3 con 524 pasosOK
Entrada en varias líneas2 3 4 5 6 7Hilo 1: n=2 pasos=1 Hilo 2: n=3 pasos=7 Hilo 3: n=4 pasos=2 Hilo 4: n=5 pasos=5 Hilo 5: n=6 pasos=8 Hilo 6: n=7 pasos=16 Maximo: hilo 6 con 16 pasosHilo 1: n=2 pasos=1 Hilo 2: n=3 pasos=7 Hilo 3: n=4 pasos=2 Hilo 4: n=5 pasos=5 Hilo 5: n=6 pasos=8 Hilo 6: n=7 pasos=16 Maximo: hilo 6 con 16 pasosOK
Entrada vacía(vacía)Entrada incorrectaEntrada incorrectaOK
Valor cero fuera de rango5 3 0 2 4 8Entrada incorrectaEntrada incorrectaOK

Salidas reales: código compilado con gcc 14.4.0 (C17) y ejecutado en un contenedor aislado el 27 de octubre de 2026.

Complejidad

Llamemos L(n) al número de pasos de Collatz de n. El trabajo total es la suma de los seis L(n) más un coste constante por crear y esperar seis hilos. No se conoce una cota cerrada para L(n) en general. Para el rango del enunciado (hasta 109) los valores son de unos pocos cientos, así que el tiempo es despreciable frente al coste de crear los hilos.

La memoria es constante: dos vectores de seis elementos (tareas e hilos) más la pila de cada hilo, que reserva el sistema. Como el número de hilos es fijo, el espacio adicional es O(1) respecto a los datos. Con varios núcleos el tiempo real tiende al del hilo más lento. Con uno solo, como en el entorno de corrección, se suman los seis cálculos.

Errores típicos

  • Pasar &i (la variable del bucle) como argumento de pthread_create. Cuando el hilo la lee, i puede haber cambiado ya, y dos hilos pueden acabar con el mismo identificador o con uno igual a 6.
  • Identificar los hilos con pthread_self() e imprimirlo con %d tras convertirlo a int. pthread_t es un tipo opaco, la conversión no es portable y el valor cambia en cada ejecución, así que la salida nunca coincide con la esperada.
  • Usar un contador global que cada hilo incrementa con contador += 1 para numerarse. Es una condición de carrera: el incremento no es atómico y el número que obtiene cada hilo depende del planificador.
  • Imprimir desde dentro del hilo sin ningún mecanismo de turno. Las líneas salen en un orden distinto en cada ejecución, aunque en el portátil del alumno «siempre salgan bien».
  • Leer tareas[i].pasos sin haber hecho antes pthread_join de ese hilo, o terminar main sin esperar a los hilos. Con return desde main el proceso entero termina y los hilos pendientes mueren.
  • Hacer la secuencia de Collatz con int. Con valores de entrada grandes, 3x+1 desborda, se produce comportamiento indefinido y el bucle puede no terminar nunca.

Variantes

Que cada hilo imprima su propia línea, en orden de identificador

Si el profesor exige que sea el hilo quien escriba su línea, hace falta sincronizarlos. Se usa una variable global turno protegida por un pthread_mutex_t y una variable de condición. Cada hilo calcula sin cerrojo, después bloquea el mutex y espera con pthread_cond_wait en un bucle while (turno != t->id). Cuando le toca, imprime, incrementa turno y hace pthread_cond_broadcast para despertar a los demás. Se usa broadcast y no signal porque solo uno de los que esperan es el siguiente. El cálculo sigue siendo concurrente: solo la impresión queda en orden. La salida es la misma que la de la solución principal.

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

#define NUM_HILOS 6
#define LIMITE 1000000000LL

typedef struct {
    int id;
    unsigned long long n;
    int pasos;
} Tarea;

static pthread_mutex_t cerrojo = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cambio = PTHREAD_COND_INITIALIZER;
static int turno = 1;

static void *trabajar(void *arg)
{
    Tarea *t = (Tarea *)arg;
    unsigned long long x = t->n;
    int pasos = 0;

    while (x != 1ULL) {
        x = (x % 2ULL == 0ULL) ? x / 2ULL : 3ULL * x + 1ULL;
        pasos++;
    }
    t->pasos = pasos;

    pthread_mutex_lock(&cerrojo);
    while (turno != t->id) {
        pthread_cond_wait(&cambio, &cerrojo);
    }
    printf("Hilo %d: n=%llu pasos=%d\n", t->id, t->n, t->pasos);
    turno++;
    pthread_cond_broadcast(&cambio);
    pthread_mutex_unlock(&cerrojo);
    return NULL;
}

int main(void)
{
    Tarea tareas[NUM_HILOS];
    pthread_t hilos[NUM_HILOS];

    for (int i = 0; i < NUM_HILOS; i++) {
        long long v;
        if (scanf("%lld", &v) != 1 || v < 1 || v > LIMITE) {
            printf("Entrada incorrecta\n");
            return 0;
        }
        tareas[i].id = i + 1;
        tareas[i].n = (unsigned long long)v;
        tareas[i].pasos = -1;
    }

    int creados = 0;
    for (int i = 0; i < NUM_HILOS; i++) {
        if (pthread_create(&hilos[i], NULL, trabajar, &tareas[i]) != 0) {
            fprintf(stderr, "pthread_create fallo\n");
            break;
        }
        creados++;
    }
    if (creados < NUM_HILOS) {
        /* liberar a los que esperan turno que nunca llegara */
        pthread_mutex_lock(&cerrojo);
        turno = NUM_HILOS + 100;
        pthread_mutex_unlock(&cerrojo);
        for (int i = 0; i < creados; i++) {
            pthread_cancel(hilos[i]);
            pthread_join(hilos[i], NULL);
        }
        return 1;
    }

    for (int i = 0; i < NUM_HILOS; i++) {
        pthread_join(hilos[i], NULL);
    }

    int mejor = 0;
    for (int i = 1; i < NUM_HILOS; i++) {
        if (tareas[i].pasos > tareas[mejor].pasos) {
            mejor = i;
        }
    }
    printf("Maximo: hilo %d con %d pasos\n", tareas[mejor].id, tareas[mejor].pasos);
    return 0;
}

Que cada hilo devuelva el resultado por pthread_join

En lugar de escribir en la estructura, el hilo puede devolver su resultado como valor de retorno. Para evitar conversiones dudosas entre enteros y punteros, lo limpio es reservar un int con malloc dentro del hilo y devolver su dirección. main lo recoge con pthread_join(hilos[i], &ret), lo copia y hace free(ret). Nunca se debe devolver la dirección de una variable local del hilo, porque su pila deja de existir al terminar.

En los temarios

La creación y gestión de hilos forma parte del temario de Sistemas Operativos en varias universidades latinoamericanas. El programa de Sistemas Operativos de la Universidad Tecnológica Nacional (FRBA) incluye la planificación e hilos en procesos, y también la comunicación y sincronización entre procesos. La sumilla de Ingeniería Informática de la Pontificia Universidad Católica del Perú recoge «Procesos e Hilos» y la comunicación entre procesos dentro de Sistemas Operativos.

En Chile, el programa de IIC2333 Sistemas Operativos y Redes de la Pontificia Universidad Católica de Chile trata los procesos, su planificación y la sincronización y comunicación entre ellos. Por su parte, CC3301 de la Universidad de Chile trabaja la programación de sistemas sobre la API de Linux con procesos pesados (fork/exec/pipes), que es el contexto natural donde después aparecen los hilos POSIX.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion · Revisado por Kernel

Kernel

· Dirección de tecnología · España

«Un programa con hilos que solo es correcto si el planificador coopera no es correcto: ha tenido suerte, y la suerte no se despliega.»