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:
- Un hilo espera a que otro termine: el hilo A hace trabajo, luego señala al hilo B para que continúe.
- Dos hilos se encuentran: ambos esperan a que el otro llegue a un punto antes de continuar.
- Un hilo espera un dato: el productor señala al consumidor cuando hay datos disponibles.
2. Ordenar la ejecución
Para ordenar la ejecución de dos hilos, se usa un semáforo con valor inicial 0:
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:
#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:
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:
#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:
- Pool de conexiones: un semáforo con valor N limita a N conexiones simultáneas.
- Buffer acotado: un semáforo cuenta los espacios disponibles en el buffer.
- Rate limiting: un semáforo limita el número de operaciones por segundo.
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:
#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ón | Descripción | Semáforos |
|---|---|---|
| Señal simple | Un hilo señala a otro que un evento ocurrió. | 1 (valor 0) |
| Rendezvous | Dos hilos se esperan mutuamente en un punto. | 2 (valor 0 cada uno) |
| Limitación | Limitar el número de hilos concurrentes. | 1 (valor N) |
| Productor-consumidor | Un 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
- La señalización usa un semáforo con valor 0 para ordenar la ejecución de hilos.
- Un hilo hace post para señalar; otro hace wait para esperar.
- El rendezvous usa dos semáforos para que dos hilos se esperen mutuamente.
- La limitación de recursos usa un semáforo con valor N para limitar la concurrencia.
- Los patrones más comunes son: señal simple, rendezvous, limitación y productor-consumidor.
- La señalización es diferente de la exclusión mutua: no protege datos, coordina ejecución.
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
- Implementa el patrón de señal simple y verifica el orden.
- Implementa el rendezvous y verifica que ambos hilos se sincronizan.
- Implementa un pool de 3 conexiones y observa la salida.
- Modifica el rendezvous para que un hilo espere más que el otro.
- Usa sem_trywait para verificar si la señal ya se envió.
- Investiga cómo se implementa el rendezvous en otros lenguajes (Go channels, Java CountDownLatch).