# Concurrencia en Rust: Send, Sync y por qué el compilador te quita el miedo

Durante años usé Go para todo lo que no hacía con Rails, y Go hizo de la concurrencia su argumento de venta: `go f()`, canales y `sync.Mutex`. Ahora ese hueco lo ocupa Rust, y lo interesante de sus hilos no es la sintaxis, sino que el compilador sabe cosas sobre ellos que el de Go ignora.

Este es el último post de la serie. En [Ownership en Rust](/post/ownership-en-rust) asomaron `Arc` y `Mutex`, y en [Borrowing en Rust](/post/borrowing-en-rust) vimos que la regla de «muchos lectores o un solo escritor» hace imposible escribir una carrera de datos. Hoy toca la maquinaria que lleva esa regla a los hilos: dos traits, `Send` y `Sync`. Todo está compilado con Rust 1.98 y la edición 2024; los errores son salida real de `cargo`, recortada.

## Send y Sync: dos traits sin métodos

La documentación de la biblioteca estándar los define en una línea cada uno:

- **`Send`**: tipos que se pueden transferir de un hilo a otro.
- **`Sync`**: tipos cuyas referencias se pueden compartir entre hilos. La definición precisa es que `T` es `Sync` si y solo si `&T` es `Send`.

No tienen métodos: son *marker traits*, etiquetas sobre el tipo. Y son *auto traits*: los deduce el compilador. Un struct es `Send` si todos sus campos lo son, y basta un campo que no lo sea para que deje de serlo. Además son `unsafe`: implementarlos a mano exige `unsafe impl`, porque, como avisa el Rustonomicon, hacerlo mal provoca comportamiento indefinido.

El caso de manual es `Rc`. Su contador no es atómico: si dos hilos clonaran a la vez un `Rc` del mismo valor, lo actualizarían sin sincronizar. Así que `Rc` no es `Send` ni `Sync`. En el post de ownership enseñé el error recortado; completo enseña algo más interesante, cómo se propaga la etiqueta:

```rust
use std::rc::Rc;
use std::thread;

struct Session {
    user: String,
    theme: Rc<String>, // compartido con otras sesiones del mismo hilo
}

fn render(s: Session) {
    println!("{} usa el tema {}", s.user, s.theme);
}

fn main() {
    let theme = Rc::new(String::from("github"));
    let s = Session { user: String::from("javier"), theme: Rc::clone(&theme) };
    let h = thread::spawn(move || render(s));
    h.join().unwrap();
}
```

```text
error[E0277]: `Rc<String>` cannot be sent between threads safely
  --> src/main.rs:16:27
   |
16 |     let h = thread::spawn(move || render(s));
   |             ------------- -------^^^^^^^^^^
   |             |             |
   |             |             `Rc<String>` cannot be sent between threads safely
   |             required by a bound introduced by this call
note: required because it appears within the type `Session`
note: required because it's used within this closure
note: required by a bound in `spawn`
```

Las tres notas se leen de abajo arriba: `spawn` exige que el closure sea `Send`; el closure contiene un `Session`; `Session` contiene un `Rc<String>`, y ahí se rompe la cadena. Nadie ha escrito que `Session` no sea `Send`: el compilador lo deduce de los campos, y lo seguirá deduciendo cuando alguien añada uno dentro de un año. La solución es `Arc<String>`.

Algunos casos que conviene conocer:

| Tipo | `Send` | `Sync` | Por qué |
|---|---|---|---|
| `Rc<T>` | no | no | contador no atómico |
| `Arc<T>` | sí* | sí* | contador atómico |
| `Cell<T>`, `RefCell<T>` | sí* | no | mutan a través de `&` sin sincronizar |
| `Mutex<T>` | sí* | sí* | serializa el acceso |
| `MutexGuard<T>` | no | sí* | con pthreads, el lock se libera en el hilo que lo tomó |

El asterisco significa «si `T` cumple lo que toque»: `Arc<T>` pide `T: Send + Sync`, y `Mutex<T>`, solo `T: Send`. `RefCell` es el caso instructivo: puedes **mandarlo** a otro hilo, donde tendrá un único dueño, pero no **compartirlo**, porque su contador de préstamos tampoco es atómico.

## Hilos: spawn, move y JoinHandle

`thread::spawn` lanza un hilo del sistema operativo y devuelve un `JoinHandle<T>`, donde `T` es lo que devuelve el closure. Como el hilo puede vivir más que quien lo lanza, el closure tiene que ser `'static` (lo vimos en el post de borrowing) y lo normal es escribirlo con `move` para que se lleve sus datos en propiedad:

```rust
use std::thread;

fn main() {
    let body = String::from("cada valor tiene un dueño y cada hilo también");

    // move: el hilo se queda con `body`; el resultado vuelve por el JoinHandle
    let counter = thread::spawn(move || body.split_whitespace().count());
    let broken = thread::spawn(|| -> usize { panic!("front matter sin título") });

    println!("palabras: {}", counter.join().unwrap());
    if broken.join().is_err() {
        println!("el hilo murió; main sigue");
    }
}
```

```text
thread '<unnamed>' (3809868) panicked at src/main.rs:8:46:
front matter sin título
palabras: 9
el hilo murió; main sigue
```

Dos diferencias con Go. Una goroutine no devuelve nada y su resultado sale por un canal o una variable compartida; aquí vuelve tipado por `join()`. Y en Go, un `panic` sin `recover` en cualquier goroutine tumba el programa entero. El mismo `panic("front matter sin título")` en una goroutine termina el proceso con código de salida 2. En Rust, el pánico de un hilo llega a quien lo espera como un `Err` de `join()`.

Cuando no quieres mover los datos, sino prestarlos, está `thread::scope`, estable desde Rust 1.63. Los hilos lanzados dentro del ámbito se esperan automáticamente antes de que `scope` devuelva, así que pueden tomar prestado lo que vive fuera sin `'static`, sin `Arc` y sin clonar:

```rust
use std::thread;

fn count_words(posts: &[String]) -> usize {
    posts.iter().map(|p| p.split_whitespace().count()).sum()
}

fn main() {
    let posts = [
        String::from("Ownership en Rust"),
        String::from("Borrowing en Rust"),
        String::from("Traits y genéricos en Rust"),
        String::from("Option y Result en Rust"),
    ];
    let (a, b) = posts.split_at(posts.len() / 2);

    let total = thread::scope(|s| {
        let h1 = s.spawn(|| count_words(a)); // préstamo, sin Arc ni clone
        let h2 = s.spawn(|| count_words(b));
        h1.join().unwrap() + h2.join().unwrap()
    }); // aquí ya han terminado todos los hilos del ámbito

    println!("{total} palabras en {} posts", posts.len());
}
```

Es el equivalente estructurado de un `sync.WaitGroup`, con una diferencia: en Go, olvidar el `wg.Wait()` compila; aquí no hay nada que olvidar, porque `scope` no devuelve hasta que terminan todos sus hilos, y por eso el compilador acepta los préstamos.

## Estado compartido: el mutex es dueño del dato

Esta es la diferencia que más pesa. Una versión de juguete del store de este blog, que está escrito en Go, con un método que se salta el lock a propósito:

```go
type Store struct {
	mu    sync.RWMutex
	posts map[string]string // protegido por mu (lo dice este comentario)
}

func (s *Store) Count() int {
	return len(s.posts) // sin lock: compila, y go vet no protesta
}
```

En Go, el mutex y el dato son dos campos vecinos, y la relación entre ellos vive en la cabeza de quien lo escribió o, con suerte, en un comentario. Nada impide leer `posts` sin pasar por `mu`, y `-race` solo lo cazará si ese camino se ejecuta a la vez que una escritura durante la prueba.

En Rust, `Mutex<T>` y `RwLock<T>` **envuelven** el dato. No hay forma de llegar al `HashMap` sin pedirle el lock:

```rust
use std::collections::HashMap;
use std::sync::RwLock;

struct Store {
    posts: RwLock<HashMap<String, String>>, // el lock es dueño del mapa
}

impl Store {
    fn upsert(&self, slug: &str, title: &str) {
        let mut posts = self.posts.write().unwrap(); // guard de escritura
        posts.insert(slug.to_string(), title.to_string());
    } // el guard muere aquí y libera el lock

    fn count(&self) -> usize {
        self.posts.len() // sin pasar por el lock
    }
}
```

```text
error[E0599]: no method named `len` found for struct `std::sync::RwLock<T>` in the current scope
  --> src/main.rs:15:20
   |
15 |         self.posts.len() // sin pasar por el lock
   |                    ^^^ method not found in `std::sync::RwLock<HashMap<String, String>>`
   |
help: use `.read().unwrap()` to borrow the `HashMap<String, String>`, blocking the current thread until it can be acquired
```

`write()` y `read()` devuelven un **guard**, RAII puro como el `Drop` de [Ownership en Rust](/post/ownership-en-rust): mientras vive, da acceso al dato; al morir, suelta el lock. No hay `Unlock` que olvidar, y el borrow checker impide que una referencia al mapa sobreviva al guard. Aaron Turon lo resumió en 2015, en el artículo del blog oficial que popularizó lo de *fearless concurrency*: Rust no te deja acceder a datos protegidos por un lock salvo que tengas el lock.

El `.unwrap()` tiene su porqué: si un hilo entra en pánico con el lock tomado, el mutex queda **envenenado** (*poisoned*) y los demás reciben un `Err`, porque el dato puede haber quedado a medias. Es un aviso, no una barrera: `PoisonError::into_inner` devuelve el guard igualmente.

Para un contador basta un **atómico** de `std::sync::atomic`, que se modifica a través de `&`. El contador que perdía incrementos en [Go frente a Rust](/post/go-frente-a-rust-fase-a-fase), arreglado:

```rust
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;

static VIEWS: AtomicU64 = AtomicU64::new(0);

fn main() {
    thread::scope(|s| {
        for _ in 0..4 {
            s.spawn(|| {
                for _ in 0..1000 {
                    VIEWS.fetch_add(1, Ordering::Relaxed);
                }
            });
        }
    });
    println!("{}", VIEWS.load(Ordering::Relaxed)); // siempre 4000
}
```

`Relaxed` basta porque solo importa el total, que se lee tras esperar a los hilos.

## Paso de mensajes: mpsc frente a los canales de Go

Go tiene un lema que viene de *Effective Go*: «*Do not communicate by sharing memory; instead, share memory by communicating*». Andrew Gerrand le dedicó un post en el blog de Go en 2010, y Rob Pike lo puso el primero de sus *Go Proverbs* en 2015. En la biblioteca estándar de Rust, el equivalente es `std::sync::mpsc`: *multi-producer, single-consumer*.

```rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<String>();

    for worker in ["watcher", "scheduler"] {
        let tx = tx.clone(); // varios productores
        thread::spawn(move || {
            for i in 1..=2 {
                tx.send(format!("{worker}: evento {i}")).unwrap();
            }
        }); // el clon de tx muere aquí
    }
    drop(tx); // suelto el original: si no, el bucle de abajo no acabaría

    for event in rx {
        println!("{event}"); // termina cuando no queda ningún Sender vivo
    }
}
```

La salida cambia de orden en cada ejecución, como con goroutines. Frente a los canales de Go:

- **El cierre es implícito.** En Go cierras con `close(ch)`, y cerrar dos veces o enviar a un canal cerrado provoca un pánico. Aquí el canal se cierra cuando muere el último `Sender`: otra vez `Drop`.
- **Con buffer o sin él.** `channel()` no tiene límite; `sync_channel(n)` es el canal con buffer de Go, y con `n = 0` cada envío espera a que alguien reciba, como uno sin buffer.
- **No hay `select`.** Desde Rust 1.67, `mpsc` está implementado sobre el código de `crossbeam-channel`, y es esa crate la que ofrece `select!` y canales con varios consumidores.

La diferencia de fondo es que **enviar es mover**. `send` toma el valor en propiedad, y después no puedes tocarlo:

```text
error[E0382]: borrow of moved value: `event`
 --> src/main.rs:9:29
  |
8 |         tx.send(event).unwrap();
  |                 ----- value moved here
9 |         println!("enviado: {event}");
  |                             ^^^^^ value borrowed here after move
```

En Go puedes mandar un puntero o un slice por un canal y seguir modificándolo desde el emisor: el lema es un consejo. En Rust es una regla del sistema de tipos.

## Lo que Rust no previene

Aquí conviene ser honesto, porque *fearless* se presta a malentendidos. El Rustonomicon lo dice claro: Rust seguro garantiza la ausencia de **carreras de datos**, y «*Rust does not prevent general race conditions*».

**Deadlocks.** Dos hilos que toman dos mutex en orden inverso compilan sin una queja:

```rust
s.spawn(|| {
    let _p = posts.lock().unwrap();
    thread::sleep(Duration::from_millis(10));
    let _v = views.lock().unwrap(); // espera a que el otro suelte views
});
s.spawn(|| {
    let _v = views.lock().unwrap();
    thread::sleep(Duration::from_millis(10));
    let _p = posts.lock().unwrap(); // espera a que el otro suelte posts
});
```

El programa se queda colgado para siempre. Tomar dos veces el mismo `Mutex` desde un hilo tampoco está prohibido: según la documentación, `lock()` no volverá, y puede entrar en pánico o bloquearse. Aquí Go tiene un detalle a su favor: el mismo programa con goroutines muere con `fatal error: all goroutines are asleep - deadlock!`. Pero solo si **todas** están bloqueadas; si otra goroutine tiene abierto un servidor HTTP, el detector no salta y se cuelga igual.

**Condiciones de carrera lógicas.** Si compruebas que un slug está libre con un lock y lo insertas con otro, entre medias puede colarse otro hilo. Cada acceso es seguro y el resultado es incorrecto. Por eso el store de este blog tiene un lock de escrituras que cubre la operación entera, y en Rust haría falta exactamente igual.

**Fugas.** Un ciclo de `Arc`, un `mem::forget` o un hilo que nunca termina son seguros en el sentido técnico, como vimos en el post de ownership.

`Send` y `Sync` eliminan una familia entera de bugs, la más difícil de reproducir. El diseño de la concurrencia sigue siendo cosa tuya.

## Async, en pocas líneas

`async fn` no ejecuta nada: devuelve un `Future` que alguien tiene que sondear, y la biblioteca estándar no trae ese ejecutor; en la práctica lo pone **Tokio**. Sus tareas son **cooperativas**: solo ceden el hilo en un `.await`, y un cálculo largo o una llamada bloqueante frena a las demás tareas de ese hilo (para eso está `spawn_blocking`). Las goroutines son **preemptivas**: desde Go 1.14 el runtime puede interrumpirlas incluso en un bucle sin llamadas a funciones.

Lo interesante aquí es que `Send` reaparece: el runtime multihilo de Tokio puede mover una tarea de hilo en cada `.await`, así que `tokio::spawn` exige un future `Send`. Un fragmento compilado con Tokio 1.53:

```rust
let task = tokio::spawn(async {
    let theme = Rc::new("github"); // vive a través del .await
    let title = fetch_title("concurrencia-en-rust-send-y-sync").await;
    println!("{title} ({theme})");
});
```

```text
error: future cannot be sent between threads safely
note: future is not `Send` as this value is used across an await
   |
12 |         let theme = Rc::new("github"); // vive a través del .await
   |             ----- has type `Rc<&str>` which is not `Send`
13 |         let title = fetch_title("concurrencia-en-rust-send-y-sync").await;
   |                                                                     ^^^^^ await occurs here, with `theme` maybe used later
```

Es el razonamiento de los hilos aplicado a la máquina de estados que el compilador genera para el bloque `async`: lo que sobrevive a un `.await` forma parte del future, y si no es `Send`, el future tampoco. Con `Arc` compila.

## Cierre de la serie

Cinco posts, una idea vista desde cinco sitios. [Ownership](/post/ownership-en-rust) dice quién libera cada valor; [Borrowing](/post/borrowing-en-rust), quién puede tocarlo mientras tanto; [Traits y genéricos](/post/traits-y-genericos-en-rust), cómo se describen las capacidades de un tipo; [Option y Result](/post/option-y-result-en-rust), cómo se convierte la ausencia y el error en algo que el compilador obliga a mirar. Y hoy, cómo esas cuatro piezas, más dos traits sin métodos, convierten una carrera de datos en un error de compilación.

El libro de Rust promete que la *fearless concurrency* te deja escribir código sin bugs sutiles y refactorizarlo sin introducir otros nuevos. Después de años de `go test -race` y de mutex que viven al lado del dato en vez de alrededor, la versión que yo firmaría es más modesta: el miedo que desaparece es a las carreras de datos. Los deadlocks siguen siendo míos.
