1. El modelo de memoria compartida
En el modelo de memoria compartida, los hilos comparten el espacio de direcciones y se sincronizan con mutex, semáforos y variables de condición. Es el modelo que hemos usado en todo el curso.
Ventajas:
- Rápido: compartir datos es trivial (un puntero).
- Flexible: se puede compartir cualquier dato.
- Soportado por el hardware: las cachés y la memoria compartida son nativas.
Desventajas:
- Condiciones de carrera: el estado mutable compartido es la fuente de errores.
- Complejidad: la sincronización es difícil de razonar.
- No escala bien: la contención limita el rendimiento.
2. El modelo de paso de mensajes
En el modelo de paso de mensajes, los procesos o hilos no comparten memoria. Se comunican enviándose mensajes a través de canales. El estado es privado a cada proceso; la comunicación es explícita.
La idea es: «compartir comunicando, en lugar de comunicar compartiendo» (Rob Pike). En lugar de proteger datos compartidos con mutex, se envían los datos a través de canales.
Ventajas:
- Sin condiciones de carrera: no hay estado mutable compartido.
- Más fácil de razonar: la comunicación es explícita.
- Escalable: no hay contención por locks.
Desventajas:
- Copia de datos: los mensajes se copian, lo que puede ser costoso.
- Overhead de comunicación: enviar un mensaje es más costoso que leer una variable compartida.
- No todo se puede expresar fácilmente: algunas estructuras de datos son difíciles de expresar con mensajes.
3. CSP: Communicating Sequential Processes
CSP (Communicating Sequential Processes) es un formalismo propuesto por Tony Hoare en 1978. Es la base teórica de los canales en Go y en otros lenguajes.
En CSP, los procesos se comunican a través de canales sincrónicos: un envío bloquea hasta que el receptor recibe el mensaje, y viceversa. No hay buffers: la comunicación es un encuentro (rendezvous).
CSP ha influido en el diseño de muchos lenguajes y sistemas: Go, Rust, Erlang, Occam, etc.
4. Canales en Go
Go (Golang) tiene canales como primitiva del lenguaje. Un canal es una cola tipada que conecta goroutines (hilos ligeros de Go):
package main
import "fmt"
func main() {
/* Canal sincrónico (sin buffer). */
ch := make(chan int)
go func() {
ch <- 42 /* enviar */
}()
val := <-ch /* recibir */
fmt.Println(val) /* imprime 42 */
}
Los canales de Go pueden ser sincrónicos (sin buffer) o asincrónicos (con buffer). Los canales sincrónicos implementan un rendezvous: el envío bloquea hasta que el receptor recibe.
Go también tiene select, que permite esperar en múltiples canales simultáneamente:
select {
case val := <-ch1:
fmt.Println("Recibido de ch1:", val)
case val := <-ch2:
fmt.Println("Recibido de ch2:", val)
case ch3 <- 42:
fmt.Println("Enviado a ch3")
default:
fmt.Println("Ningún canal listo")
}
5. Canales en Rust
Rust proporciona canales a través de la biblioteca estándar (std::sync::mpsc):
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(42).unwrap();
});
let val = rx.recv().unwrap();
println!("{}", val); /* imprime 42 */
}
Rust también soporta canales sincrónicos (sync_channel) con capacidad limitada.
6. El modelo de actores
El modelo de actores es un modelo de concurrencia en el que los actores son entidades independientes que se comunican por mensajes. Cada actor tiene su propio estado (privado) y procesa mensajes de una cola. Los actores no comparten memoria.
El modelo de actores fue propuesto por Carl Hewitt en 1973. Es la base de Erlang, Akka (Scala/Java) y otros sistemas.
Los actores tienen estas propiedades:
- Estado privado: cada actor tiene su propio estado, no compartido.
- Cola de mensajes: cada actor tiene una cola de mensajes entrantes.
- Procesamiento secuencial: cada actor procesa un mensaje a la vez.
- Comunicación asíncrona: los mensajes se envían sin esperar respuesta.
- Sin locks: no se necesitan mutex ni semáforos.
7. Actores en Erlang/Elixir
Erlang es el lenguaje que mejor implementa el modelo de actores. Cada proceso (actor) es ligero (creación en ~1 μs) y se comunica por mensajes:
%% Crear un proceso (actor).
Pid = spawn(fun() -> loop() end).
%% Enviar un mensaje.
Pid ! {hello, "world"}.
%% Recibir un mensaje.
receive
{hello, Msg} -> io:format("Recibido: ~s~n", [Msg]);
{bye} -> io:format("Adiós~n")
end.
Erlang es famoso por su tolerancia a fallos: los actores se supervisan mutuamente y se reinician si fallan. Es la base de sistemas de telecomunicaciones (Ericsson) y de aplicaciones distribuidas.
8. JavaScript: un solo hilo
JavaScript usa un modelo de concurrencia basado en un event loop con un solo hilo. No hay hilos en el código de usuario: toda la concurrencia se logra con callbacks, promesas y async/await.
Ventajas:
- Sin condiciones de carrera: un solo hilo ejecuta el código de usuario.
- Sin locks: no se necesitan mutex ni semáforos.
- Simple: el modelo mental es más simple.
Desventajas:
- No aprovecha múltiples núcleos: un solo hilo no puede usar más de un núcleo.
- Bloqueo: una operación bloqueante bloquea todo el programa.
- Callbacks hell: código anidado difícil de leer.
Web Workers permiten ejecutar código en hilos separados, pero la comunicación es por mensajes (postMessage), no por memoria compartida.
9. Comparación de modelos
| Modelo | Memoria compartida | Sincronización | Condiciones de carrera | Ejemplo |
|---|---|---|---|---|
| Memoria compartida | Sí | Mutex, semáforos, atómicos | Sí (si no se sincroniza) | C, Java, pthreads |
| Paso de mensajes | No | Canales, send/receive | No | Go, Rust, CSP |
| Actores | No | Mensajes asíncronos | No | Erlang, Akka |
| Event loop | No (en el código de usuario) | Callbacks, promesas | No (en el código de usuario) | JavaScript, Node.js |
10. Simulador interactivo
Este simulador muestra la diferencia entre memoria compartida y paso de mensajes:
Simulador · modelos de concurrencia
11. Resumen
- El modelo de memoria compartida usa mutex y semáforos para proteger datos compartidos.
- El modelo de paso de mensajes usa canales para comunicar entre procesos.
- CSP es la base teórica de los canales en Go y Rust.
- El modelo de actores usa mensajes asíncronos entre entidades independientes.
- JavaScript usa un event loop con un solo hilo.
- Cada modelo tiene ventajas y desventajas.
- La elección del modelo depende del problema.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué es el modelo de paso de mensajes?
Un modelo donde los procesos se comunican enviándose mensajes a través de canales, sin compartir memoria.
2. ¿Qué es CSP?
Communicating Sequential Processes: un formalismo de Hoare que es la base de los canales en Go.
3. ¿Cómo funcionan los canales en Go?
Son colas tipadas que conectan goroutines. Pueden ser sincrónicos o asincrónicos.
4. ¿Qué es el modelo de actores?
Entidades independientes con estado privado que se comunican por mensajes asíncronos.
5. ¿Cómo maneja JavaScript la concurrencia?
Con un event loop de un solo hilo, usando callbacks, promesas y async/await.
6. ¿Qué ventaja tiene el paso de mensajes sobre la memoria compartida?
No hay condiciones de carrera (no hay estado mutable compartido).
7. ¿Qué desventaja tiene el paso de mensajes?
Copia de datos y overhead de comunicación.
8. ¿Qué es un canal sincrónico?
Un canal sin buffer donde el envío bloquea hasta que el receptor recibe (rendezvous).
9. ¿Qué es select en Go?
Una construcción que permite esperar en múltiples canales simultáneamente.
10. ¿Por qué JavaScript no aprovecha múltiples núcleos?
Porque usa un solo hilo para el código de usuario.
Para practicar
- Escribe un programa en Go que use canales para comunicar dos goroutines.
- Implementa un productor-consumidor con canales en Go.
- Compara el modelo de actores con el de memoria compartida para un problema simple.
- Investiga cómo Erlang maneja la tolerancia a fallos.
- Explica por qué JavaScript no tiene condiciones de carrera en el código de usuario.
- Investiga Web Workers y cómo se comunican con el hilo principal.