Comunicación y Sincronización de Procesos

Comunicación y Sincronización de Procesos

Cuando varios procesos (o hilos) conviven en el mismo sistema operativo, casi siempre necesitan interactuar de alguna forma. Unos generan datos que otros consumen, varios leen la misma configuración, un proceso avisa a otro que ya terminó su parte crítica… y sin reglas claras todo esto termina en resultados impredecibles, datos corruptos o bloqueos totales.

Hoy repasamos los conceptos centrales de comunicación entre procesos (IPC) y sincronización, sin tablas ni listas largas, solo texto fluido y ejemplos concretos.

¿Cómo se comunican los procesos?

Básicamente existen dos filosofías principales:

1. Comparten memoria

   Los procesos acceden a la misma región de memoria (ya sea compartida por el SO o mapeada manualmente). Es la opción más rápida porque no hay que copiar datos de un lado a otro.  

   Ejemplo típico: un productor escribe frames de video en un buffer grande y el consumidor (el reproductor) los lee directamente desde el mismo lugar.  

   El precio que se paga es que toda la responsabilidad de no pisarse recae en los programadores.


2. Se envían mensajes

   Un proceso prepara un mensaje y el sistema operativo se encarga de entregarlo al destinatario (pipes, colas de mensajes, sockets locales, etc.).  

   Aquí el kernel hace de intermediario, lo que suele ser más lento porque implica copiar datos, pero también es más seguro porque cada proceso no tiene punteros directos a la memoria del otro.


En sistemas tipo UNIX/Linux los mecanismos más usados son pipes (anónimos o con nombre), colas de mensajes POSIX, memoria compartida (shm_open + mmap), sockets de dominio UNIX y señales (aunque estas últimas más bien notifican que transferir datos grandes).


La parte más importante: sincronización

La comunicación casi nunca es suficiente sola. Casi siempre hay que responder dos preguntas clave:


- ¿Quién puede entrar ahora a esta zona crítica? (exclusión mutua)  

- ¿Ya terminó el otro lo que tenía que hacer para que yo pueda seguir? (orden / precedencia)


Los problemas más famosos que ilustran estas necesidades son:


- Productor–Consumidor (buffer finito: no se puede escribir si está lleno, no se puede leer si está vacío)  

- Lectores–Escritores (muchos pueden leer al mismo tiempo, pero cuando alguien escribe nadie más puede ni leer ni escribir)  

- Filósofos cenando (evitar que todos agarren el tenedor izquierdo y se queden trabados para siempre)  

- Barbero dormilón (el barbero duerme si no hay clientes, los clientes esperan si el barbero está cortando pelo)


Un ejemplo clásico con semáforos (productor-consumidor)

Imagina un buffer circular de tamaño fijo.

Tenemos tres semáforos:

- uno que cuenta espacios vacíos (inicia con el tamaño del buffer)  

- otro que cuenta elementos llenos (inicia en 0)  

- y un tercero para proteger el acceso al buffer (exclusión mutua, inicia en 1)

El productor hace:

producir dato → esperar un espacio vacío → tomar el mutex → escribir en la posición siguiente → soltar el mutex → avisar que hay un elemento más

El consumidor hace lo inverso:

esperar que haya al menos un elemento → tomar el mutex → leer el dato → avanzar la posición de lectura → soltar el mutex → avisar que hay un espacio libre

Con solo estos tres semáforos bien ordenados se resuelve el problema sin condiciones de carrera ni desperdicio de CPU (no hay espera activa).


Herramientas más comunes en la práctica actual


- Mutex → el más sencillo para proteger una sección crítica corta  

- Semáforo → cuando hay que contar recursos (espacios libres, conexiones disponibles, etc.)  

- Variable de condición + mutex → muy usado en monitores (Java, pthreads, Rust std::sync::Condvar)  

- Spinlock → cuando la espera va a ser muy corta y no queremos invocar al planificador  

- Read-write lock → optimizado para el caso lectores >> escritores  

- Barreras → todos los hilos deben llegar a cierto punto antes de continuar


Resumen para recordar rápido

Comunicación = cómo pasan datos (memoria compartida vs. mensajes)  

Sincronización = cómo evitan pisarse y respetan el orden (exclusión mutua + coordinación)


Los errores más frecuentes en exámenes y proyectos son:  

- Olvidar señalar un semáforo después de una espera  

- Tomar dos mutex en distinto orden en diferentes partes del código (→ posible deadlock)  

- Usar memoria compartida sin ningún mecanismo de sincronización  

- Hacer busy-waiting innecesario cuando se podría dormir con semáforos o condvars

¿Cuál de estos conceptos o problemas clásicos te ha costado más trabajo implementar o entender? ¿Has tenido que depurar algún deadlock o condición de carrera en C, Java, Go o Rust? Cuéntame abajo.


¡La próxima entrada irá sobre interbloqueos: cómo se producen, cómo detectarlos y (sobre todo) cómo evitarlos!


Nos leemos pronto. 🚀

Video:


Comentarios

Entradas populares