Concurrencia en Rust
03/07/2017
Rust destaca por su soporte nativo a la concurrencia. Aquí veremos exactamente qué es lo que hace.
Todas las funciones relacionadas con threads están en std::thread así que primero hemos de importarlo con use. Para crear un thread usamos thread::spawn, que toma una función como argumento. Esta operación devuelve un JoinHandler que puede ser usado para esperar a que finalicen los hilos de ejecución.
Aquí ya se presenta una cuestión interesante, ¿qué pasa si queremos llevar datos del hilo principal al nuevo thread? Si lo intentamos hacer, Rust se quejará. En realidad, tenemos que activar la closure con move. Lo que hace es transferir la propiedad del dato a la closure. Con eso podemos procesarlo tranquilamente en el hilo nuevo, siempre y cuando no intentemos acceder a él desde el hilo principal.
Una de las opciones que permite Rust para concurrencia es el paso de mensajes.
Tx es el objeto usado para transmitir y Rx para recibir. Tx lo puede tener cualquier hilo para mandar mensajes mientras que Rx está limitado al hilo principal. Existen varias formas de recibir los mensajes. try_recv no bloquea el hilo de ejecución, por lo que puede usarse en un bucle con if-let. recv es síncrono y bloquea el hilo de ejecución si hace falta. Tx también es Iterable así que podemos leer los mensajes en un bucle for-in.
Cuando queremos compartir memoria en Rust tenemos que recurrir a una combinación de Mutex y Arc.
Mutex provee de gestión del acceso a memoria. Para acceder al dato es necesario hacer lock. No es estrictamente necesario hacer unlock una vez hayamos modificado el dato, pues cuando Rust libere memoria, también hará unlock. Sin embargo, Mutex por sí solo no es suficiente.
Arc (Atomic Reference Counter) permite tener datos con múltiples dueños. Esto funciona con una especie de recolector de basura y es útil porque cuando hablamos de hilos podría darse la situación de que el hilo dueño muriese y un hilo con los datos prestados todavía siguiese vivo. Con clone conseguimos una copia para mover libremente que en realidad referencia al mismo dato en memoria.
Con esto ya tendremos lo suficiente para manejar concurrencia en Rust. Por supuesto, esto es más complejo que esto y si de verdad quieres aprovechar el potencial al máximo quizá debas revisar la documentación de las traits Send y Sync.
Crear threads
Todas las funciones relacionadas con threads están en std::thread así que primero hemos de importarlo con use. Para crear un thread usamos thread::spawn, que toma una función como argumento. Esta operación devuelve un JoinHandler que puede ser usado para esperar a que finalicen los hilos de ejecución.
use std::thread;
fn main(){
let handle = thread::spawn(||{
for i in 1..100{
println!("Attens ou va t'en - France Gall");
}
});
handle.join();
println!("Hilo finalizado");
}
Aquí ya se presenta una cuestión interesante, ¿qué pasa si queremos llevar datos del hilo principal al nuevo thread? Si lo intentamos hacer, Rust se quejará. En realidad, tenemos que activar la closure con move. Lo que hace es transferir la propiedad del dato a la closure. Con eso podemos procesarlo tranquilamente en el hilo nuevo, siempre y cuando no intentemos acceder a él desde el hilo principal.
use std::thread;
fn main(){
let v = vec!["Attens ou va t'en - Paul Mauriat","Attens ou va t'en - France Gall"];
let handle = thread::spawn(move ||{
for i in v{
println!("{}",i);
}
});
handle.join();
println!("Hilo finalizado");
}
Mensajes
Una de las opciones que permite Rust para concurrencia es el paso de mensajes.
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main(){
let (tx,rx) = mpsc::channel();
let h = thread::spawn(move ||{
thread::sleep(Duration::new(5,0));
let val = String::from("Attens ou va t'en - France Gall");
tx.send(val).unwrap();
});
loop {
if let Ok(msg) = rx.try_recv() {
println!("{}",msg);
break;
}
}
// Opción síncrona
//let msg = rx.recv().unwrap();
//println!("{}",msg);
// Opción Iterable
// for msg in rx{
// println!("{}",msg);
// }
}
Tx es el objeto usado para transmitir y Rx para recibir. Tx lo puede tener cualquier hilo para mandar mensajes mientras que Rx está limitado al hilo principal. Existen varias formas de recibir los mensajes. try_recv no bloquea el hilo de ejecución, por lo que puede usarse en un bucle con if-let. recv es síncrono y bloquea el hilo de ejecución si hace falta. Tx también es Iterable así que podemos leer los mensajes en un bucle for-in.
Mutex y Arc
Cuando queremos compartir memoria en Rust tenemos que recurrir a una combinación de Mutex y Arc.
Mutex provee de gestión del acceso a memoria. Para acceder al dato es necesario hacer lock. No es estrictamente necesario hacer unlock una vez hayamos modificado el dato, pues cuando Rust libere memoria, también hará unlock. Sin embargo, Mutex por sí solo no es suficiente.
Arc (Atomic Reference Counter) permite tener datos con múltiples dueños. Esto funciona con una especie de recolector de basura y es útil porque cuando hablamos de hilos podría darse la situación de que el hilo dueño muriese y un hilo con los datos prestados todavía siguiese vivo. Con clone conseguimos una copia para mover libremente que en realidad referencia al mismo dato en memoria.
use std::thread;
use std::sync::{Mutex,Arc};
fn main(){
let counter = Arc::new(Mutex::new(42));
let mut handles = vec![];
for _ in 0..10 {
let counter = counter.clone();
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Con esto ya tendremos lo suficiente para manejar concurrencia en Rust. Por supuesto, esto es más complejo que esto y si de verdad quieres aprovechar el potencial al máximo quizá debas revisar la documentación de las traits Send y Sync.