← Concurrencia y Sincronización Bloque 9 · Más allá de los candados · Tema 44 de 47

Tema 44

Otros modelos: mensajes y canales

Hasta ahora hemos usado el modelo de memoria compartida: los hilos comparten datos y se sincronizan con mutex, semáforos y variables de condición. Pero existen otros modelos: la comunicación por mensajes (canales, actores) que evitan el estado compartido mutable. En este tema verás el modelo de paso de mensajes, los canales en Go y Rust, el modelo de actores y cómo JavaScript maneja la concurrencia con un solo hilo.

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:

Desventajas:

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:

Desventajas:

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):

Go · canales
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:

Go · select
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):

Rust · canales
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:

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:

Erlang · actores
%% 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:

Desventajas:

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

ModeloMemoria compartidaSincronizaciónCondiciones de carreraEjemplo
Memoria compartidaSíMutex, semáforos, atómicosSí (si no se sincroniza)C, Java, pthreads
Paso de mensajesNoCanales, send/receiveNoGo, Rust, CSP
ActoresNoMensajes asíncronosNoErlang, Akka
Event loopNo (en el código de usuario)Callbacks, promesasNo (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

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

  1. Escribe un programa en Go que use canales para comunicar dos goroutines.
  2. Implementa un productor-consumidor con canales en Go.
  3. Compara el modelo de actores con el de memoria compartida para un problema simple.
  4. Investiga cómo Erlang maneja la tolerancia a fallos.
  5. Explica por qué JavaScript no tiene condiciones de carrera en el código de usuario.
  6. Investiga Web Workers y cómo se comunican con el hilo principal.