← Concurrencia y Sincronización Bloque 5 · Semáforos · Tema 22 de 47

Tema 22

Señalización y orden de ejecución

Los semáforos no solo sirven para contar recursos: también se usan para señalar entre hilos y ordenar su ejecución. Un hilo puede esperar a que otro lo señale antes de continuar. En este tema verás cómo usar un semáforo para ordenar la ejecución de dos hilos, el encuentro (rendezvous) de dos hilos y la limitación de recursos.

1. Qué es la señalización

La señalización (signaling) es un mecanismo por el cual un hilo notifica a otro que un evento ocurrió. El hilo que espera bloquea hasta que el otro lo señale. El semáforo con valor inicial 0 es la herramienta natural para la señalización: wait bloquea hasta que post incrementa el contador.

La señalización es diferente de la exclusión mutua: no se trata de proteger un dato compartido, sino de coordinar el orden de ejecución de los hilos.

Ejemplos de señalización:

2. Ordenar la ejecución

Para ordenar la ejecución de dos hilos, se usa un semáforo con valor inicial 0:

orden.c
sem_t sem;  /* valor inicial 0 */

/* Hilo A: hace su trabajo, luego señala a B. */
void *hilo_a(void *arg) {
    hacer_trabajo_a();
    sem_post(&sem);  /* señalar a B */
    return NULL;
}

/* Hilo B: espera a A, luego hace su trabajo. */
void *hilo_b(void *arg) {
    sem_wait(&sem);  /* esperar a A */
    hacer_trabajo_b();
    return NULL;
}

Hilo B no puede ejecutar hacer_trabajo_b() hasta que hilo A haya terminado hacer_trabajo_a() y haya hecho sem_post. Esto garantiza el orden A → B.

3. Un ejemplo de orden

Un ejemplo concreto: un hilo calcula un resultado y otro lo imprime:

orden_ejemplo.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

sem_t sem;
int resultado = 0;

void *calcular(void *arg) {
    sleep(2);  /* simular cálculo largo */
    resultado = 42;
    printf("Cálculo terminado.\n");
    sem_post(&sem);  /* señalar al impresor */
    return NULL;
}

void *imprimir(void *arg) {
    sem_wait(&sem);  /* esperar al cálculo */
    printf("Resultado: %d\n", resultado);
    return NULL;
}

int main(void) {
    pthread_t h1, h2;
    sem_init(&sem, 0, 0);

    pthread_create(&h1, NULL, calcular, NULL);
    pthread_create(&h2, NULL, imprimir, NULL);

    pthread_join(h1, NULL);
    pthread_join(h2, NULL);
    sem_destroy(&sem);
    return 0;
}

El hilo impresor espera a que el hilo calculador termine y señale. Sin el semáforo, el impresor podría leer resultado antes de que se calcule (condición de carrera).

4. El encuentro (rendezvous)

El encuentro (rendezvous) es un patrón de sincronización donde dos hilos se esperan mutuamente en un punto: ninguno puede continuar hasta que el otro llegue. Se implementa con dos semáforos:

rendezvous.c
sem_t sem_a, sem_b;  /* ambos con valor inicial 0 */

void *hilo_a(void *arg) {
    hacer_trabajo_a_parte1();
    sem_post(&sem_a);      /* señalar a B que A llegó */
    sem_wait(&sem_b);      /* esperar a que B llegue */
    hacer_trabajo_a_parte2();
    return NULL;
}

void *hilo_b(void *arg) {
    hacer_trabajo_b_parte1();
    sem_post(&sem_b);      /* señalar a A que B llegó */
    sem_wait(&sem_a);      /* esperar a que A llegue */
    hacer_trabajo_b_parte2();
    return NULL;
}

El rendezvous garantiza que ambos hilos completan su primera parte antes de que cualquiera comience la segunda. Es útil cuando dos hilos necesitan sincronizarse en un punto antes de continuar.

5. Ejemplo de rendezvous

Un ejemplo concreto: dos hilos que deben sincronizarse antes de imprimir:

rendezvous_ejemplo.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

sem_t sem_a, sem_b;

void *hilo_a(void *arg) {
    printf("A: parte 1\n");
    sleep(1);
    sem_post(&sem_a);  /* A llegó */
    sem_wait(&sem_b);  /* esperar a B */
    printf("A: parte 2\n");
    return NULL;
}

void *hilo_b(void *arg) {
    sleep(2);  /* B tarda más en la parte 1 */
    printf("B: parte 1\n");
    sem_post(&sem_b);  /* B llegó */
    sem_wait(&sem_a);  /* esperar a A */
    printf("B: parte 2\n");
    return NULL;
}

int main(void) {
    pthread_t ha, hb;
    sem_init(&sem_a, 0, 0);
    sem_init(&sem_b, 0, 0);

    pthread_create(&ha, NULL, hilo_a, NULL);
    pthread_create(&hb, NULL, hilo_b, NULL);

    pthread_join(ha, NULL);
    pthread_join(hb, NULL);

    sem_destroy(&sem_a);
    sem_destroy(&sem_b);
    return 0;
}

La salida garantiza que «A: parte 1» y «B: parte 1» se imprimen antes de «A: parte 2» y «B: parte 2». El orden de las partes 1 puede variar, pero las partes 2 siempre van después de ambas partes 1.

6. Limitación de recursos

El semáforo con valor inicial N se usa para limitar el número de hilos que acceden a un recurso simultáneamente. Esto es diferente de la señalización: no se trata de ordenar la ejecución, sino de limitar la concurrencia.

Ejemplos de limitación de recursos:

La limitación de recursos se estudia en detalle en el tema 29 (productor-consumidor).

7. Ejemplo de limitación

Un ejemplo de limitación de recursos: un pool de 3 conexiones:

pool.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define NUM_HILOS 8
#define POOL_SIZE 3

sem_t pool;

void *usar_recurso(void *arg) {
    int id = *(int *)arg;
    printf("Hilo %d: esperando recurso...\n", id);
    sem_wait(&pool);
    printf("Hilo %d: usando recurso\n", id);
    sleep(1);
    printf("Hilo %d: liberando recurso\n", id);
    sem_post(&pool);
    return NULL;
}

int main(void) {
    pthread_t hilos[NUM_HILOS];
    int ids[NUM_HILOS];
    sem_init(&pool, 0, POOL_SIZE);

    for (int i = 0; i < NUM_HILOS; i++) {
        ids[i] = i;
        pthread_create(&hilos[i], NULL, usar_recurso, &ids[i]);
    }
    for (int i = 0; i < NUM_HILOS; i++) {
        pthread_join(hilos[i], NULL);
    }
    sem_destroy(&pool);
    return 0;
}

Solo 3 hilos pueden usar el recurso simultáneamente. Los demás esperan. Cuando uno libera (post), otro entra (wait).

8. Patrones de señalización

Los patrones más comunes de señalización con semáforos son:

PatrónDescripciónSemáforos
Señal simpleUn hilo señala a otro que un evento ocurrió.1 (valor 0)
RendezvousDos hilos se esperan mutuamente en un punto.2 (valor 0 cada uno)
LimitaciónLimitar el número de hilos concurrentes.1 (valor N)
Productor-consumidorUn hilo produce, otro consume, con buffer acotado.3 (mutex + vacíos + llenos)

9. Simulador interactivo

Este simulador muestra el patrón de señalización simple: un hilo señala a otro para que continúe:

Simulador · señalización

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es la señalización?

Un mecanismo por el cual un hilo notifica a otro que un evento ocurrió.

2. ¿Cómo se implementa la señalización con semáforos?

Con un semáforo de valor inicial 0: el hilo que señala hace post, el que espera hace wait.

3. ¿Qué es un rendezvous?

Un patrón donde dos hilos se esperan mutuamente en un punto.

4. ¿Cuántos semáforos se necesitan para un rendezvous?

Dos, ambos con valor inicial 0.

5. ¿Para qué se usa la limitación de recursos?

Para limitar el número de hilos que acceden a un recurso simultáneamente.

6. ¿Cuál es la diferencia entre señalización y exclusión mutua?

La señalización coordina el orden de ejecución; la exclusión mutua protege datos compartidos.

7. ¿Qué patrón usa tres semáforos?

El productor-consumidor (mutex + vacíos + llenos).

8. ¿Por qué el semáforo con valor 0 es ideal para señalización?

Porque wait bloquea hasta que post incrementa el contador a 1.

9. ¿Qué garantiza el rendezvous?

Que ambos hilos completan su primera parte antes de que cualquiera comience la segunda.

10. ¿Qué es un pool de recursos?

Un conjunto de recursos limitados, controlado por un semáforo con valor N.

Para practicar

  1. Implementa el patrón de señal simple y verifica el orden.
  2. Implementa el rendezvous y verifica que ambos hilos se sincronizan.
  3. Implementa un pool de 3 conexiones y observa la salida.
  4. Modifica el rendezvous para que un hilo espere más que el otro.
  5. Usa sem_trywait para verificar si la señal ya se envió.
  6. Investiga cómo se implementa el rendezvous en otros lenguajes (Go channels, Java CountDownLatch).