Send: mover un valor a otro hilo
Un tipo es Send si es seguro transferir su propiedad a otro hilo. La inmensa mayoría de los tipos lo son; las excepciones —Rc y los punteros crudos— fallan por razones concretas que revelan que Send codifica la ausencia de estado compartido sin sincronizar.
Send es la más sencilla de las dos preguntas y la que gobierna el gesto más común de la concurrencia: arrancar un hilo y entregarle un dato. Un tipo es Send cuando mover un valor suyo a otro hilo —cederle la propiedad, para que el hilo original ya no lo toque— no puede provocar comportamiento indefinido. La lista de tipos que fallan esta condición es sorprendentemente corta, y estudiar por qué fallan enseña más sobre el modelo de memoria de Rust que cualquier definición abstracta.
- Definir
Sendcomo la seguridad de transferir la propiedad de un valor entre hilos. - Ver cómo
thread::spawnexigeSenda todo lo que captura la clausura. - Entender por qué
Rcno esSendy qué lo separa de un contador atómico. - Reconocer los punteros crudos como el otro gran caso
!Sendy por qué.
Mover la propiedad a través de una frontera
Cuando lanzas un hilo con una clausura move, cada valor capturado deja de pertenecer al hilo padre y pasa a pertenecer al hijo. Eso es un move como cualquiera de los del nivel 8, con una diferencia crucial: el nuevo dueño vive en otro hilo, y ambos hilos avanzan en paralelo. Send es exactamente la garantía de que ese cruce de frontera es inofensivo.
use std::thread;
fn main() {
let datos = vec![1, 2, 3]; // Vec<i32> es Send
let handle = thread::spawn(move || {
// `datos` ha sido MOVIDO aqui; el hilo padre ya no lo posee
println!("suma = {}", datos.iter().sum::<i32>());
});
handle.join().unwrap();
}
Un Vec<i32> es Send, así que el compilador acepta la mudanza sin rechistar. Como el move invalida datos en el padre (regla 2 del ownership), no hay dos dueños: solo el hilo hijo puede tocar ese buffer, y la ausencia de acceso simultáneo es precisamente lo que hace segura la operación. Send, visto así, es la extensión natural del ownership a la dimensión de los hilos: si solo hay un dueño, y ese dueño está en un único hilo, no hay carrera posible.
La regla estructural se aplica igual a tus propios tipos: un struct es Send cuando todos sus campos lo son, sin que anotes nada.
use std::thread;
struct Trabajo {
id: u64,
etiquetas: Vec<String>,
} // Send: u64 y Vec<String> lo son, luego Trabajo tambien
fn lanzar(t: Trabajo) {
thread::spawn(move || {
println!("trabajo {} con {} etiquetas", t.id, t.etiquetas.len());
});
}
Trabajo viaja a otro hilo sin ceremonia porque cada una de sus piezas es Send, y no existe ningún impl Send for Trabajo en el código: la propiedad se dedujo de los campos. Basta con que uno solo deje de ser Send para que el tipo entero pierda el permiso de cruzar la frontera.
La inmensa mayoría de los tipos que usarás a diario son Send: los enteros, bool, char, String, Vec<T>, Box<T>, HashMap, y tus propios struct y enum compuestos de piezas Send. La regla práctica se invierte: no memorices qué es Send, memoriza la corta lista de lo que no lo es. Si un tipo aparece en esa lista negra, hay siempre una razón concreta ligada a estado compartido sin sincronizar.
Por qué Rc no es Send
El contraejemplo canónico es Rc<T>, el puntero de conteo de referencias del nivel 27. Rc permite varios dueños de un mismo dato llevando la cuenta de cuántos hay; cuando el contador llega a cero, libera. El problema: ese contador es un entero normal, sin protección atómica. Clonar un Rc hace contador += 1; soltarlo hace contador -= 1.
use std::rc::Rc;
use std::thread;
fn main() {
let compartido = Rc::new(42);
let clon = Rc::clone(&compartido);
// ERROR E0277: `Rc<i32>` cannot be sent between threads safely
thread::spawn(move || {
println!("{clon}");
});
}
Imagina que Rc fuera Send y que este código compilara. El hilo padre suelta su Rc (contador -= 1) en el mismo instante en que el hijo clona el suyo (contador += 1). Esas dos operaciones no son atómicas: cada una es un ciclo de leer, modificar y escribir, y al entrelazarse pueden pisarse. El contador acaba con un valor equivocado. Si queda demasiado alto, el dato nunca se libera: fuga. Si queda demasiado bajo, se libera mientras alguien lo sigue usando: use-after-free, la vulnerabilidad clásica, resucitada. Rust corta el problema de raíz declarando Rc como !Send: nunca cruza una frontera de hilo, así que su contador jamás sufre una carrera.
Todo tipo se puede mover en Rust; Send no habla de si un valor puede moverse, sino de si moverlo a otro hilo es seguro. Rc se mueve perfectamente dentro de un mismo hilo —lo haces todo el rato—. Lo que !Send prohíbe es específicamente cruzar la frontera entre hilos. La distinción es fina y esencial: Send es una propiedad sobre hilos, no sobre movilidad.
Punteros crudos: el otro gran !Send
El segundo habitante de la lista negra son los punteros crudos, *const T y *mut T. El compilador los declara !Send (y !Sync) por una razón de principio: un puntero crudo no lleva consigo ninguna garantía. No se sabe si apunta a memoria válida, si hay otros punteros al mismo sitio, ni quién es responsable de liberarla. Mover semejante objeto a otro hilo sería firmar un cheque en blanco contra el modelo de memoria.
struct Crudo {
ptr: *mut u8, // este campo hace que Crudo sea !Send automaticamente
}
Como los auto-traits se propagan por los campos, meter un *mut u8 en un struct lo vuelve !Send sin que lo declares. Esto es deliberado: el unsafe que rodea a los punteros crudos no debe filtrarse en silencio a la concurrencia. Si de verdad sabes que tu tipo con punteros es seguro de enviar —porque, por ejemplo, gestionas la sincronización tú mismo— tendrás que afirmarlo explícitamente con unsafe impl Send, asumiendo la responsabilidad. Esa es la historia de la última lección de este nivel.
Muchos envoltorios de recursos del sistema o de bibliotecas de C guardan un puntero crudo por dentro, así que heredan el !Send automáticamente. A veces es lo correcto —ese recurso quizá solo pueda usarse desde el hilo que lo creó— y a veces es demasiado estricto, porque el recurso sí tolera moverse. Distinguir los dos casos, y actuar en consecuencia, es justo la decisión que estudiaremos en la última lección.
flowchart LR A[Valor de tipo T en el hilo padre] -->|move a la clausura| B[El hilo hijo pasa a ser dueno] B --> C[Comprobacion en compilacion, T es Send] C -->|si| OK[Compila, un solo dueno y sin carrera] C -->|no| ERR[Error E0277 en compilacion] style OK fill:#a6e3a1,color:#11111b style ERR fill:#f38ba8,color:#11111b style C fill:#89b4fa,color:#11111b
La lección profunda de Send es que no introduce ninguna idea nueva: es el principio del dueño único del nivel 8, contemplado a través de la lente de la concurrencia. Mover un valor a otro hilo es seguro por la misma razón por la que mover un valor a otra función lo es —porque el origen queda invalidado y no hay dos accesos vivos al mismo dato—. Todo lo que Send añade es cerrar la única grieta por la que esa garantía podía escaparse: los tipos que mantienen estado compartido sin sincronizar a espaldas del sistema de ownership. Rc comparte un contador; los punteros crudos comparten vaya usted a saber qué. En ambos casos existe un canal de mutación oculto que el borrow checker no vigila, y por ese canal se colaría la carrera. Send no es, entonces, una restricción arbitraria: es el ownership tapando su propio punto ciego. Cuando entiendas que !Send siempre señala “aquí hay estado compartido que yo no puedo sincronizar”, dejarás de memorizar la lista negra y empezarás a deducirla.
- Escribe una función que lance un hilo capturando un
Vec<String>conmovey verifica que compila; explica por qué elmoveelimina la posibilidad de carrera. - Sustituye el
Vecpor unRc<Vec<String>>y lee el error E0277 completo. Identifica en el mensaje la frase “cannot be sent between threads”. - Cambia
RcporArcy observa que ahora sí compila. Anota la diferencia y guárdala para la lección de la tabla. - Explica con tus palabras por qué el problema de
Rces su contador y no el dato que envuelve. - Razona por qué un
structque contenga un*mut u8es!Sendsin que nadie lo declare, y qué haría falta para revertirlo.