Transferencia de datos concurrente con canales de Rust

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Using Message Passing to Transfer Data Between Threads. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, exploramos el paso de mensajes como un enfoque seguro de concurrencia, utilizando canales de la biblioteca estándar de Rust para enviar y recibir datos entre hilos.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/PerformanceandConcurrencyGroup(["Performance and Concurrency"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/PerformanceandConcurrencyGroup -.-> rust/message_passing("Message Passing") subgraph Lab Skills rust/variable_declarations -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} rust/string_type -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} rust/for_loop -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} rust/function_syntax -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} rust/expressions_statements -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} rust/method_syntax -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} rust/message_passing -.-> lab-100438{{"Transferencia de datos concurrente con canales de Rust"}} end

Using Message Passing to Transfer Data Between Threads

Una forma cada vez más popular de garantizar una concurrencia segura es el paso de mensajes, donde los hilos o actores se comunican enviándose unos a otros mensajes que contienen datos. Aquí está la idea en un eslogan de la documentación de Go en https://golang.org/doc/effective_go.html#concurrency: "No comiences a comunicarte compartiendo memoria; en su lugar, comparte memoria comunicándote".

Para lograr una concurrencia de envío de mensajes, la biblioteca estándar de Rust proporciona una implementación de canales. Un canal es un concepto de programación general por el cual se envía datos de un hilo a otro.

Puedes imaginar un canal en programación como un canal direccional de agua, como un arroyo o un río. Si pones algo como un pato de goma en un río, viajará hacia abajo hasta el final del cauce.

Un canal tiene dos partes: un transmisor y un receptor. La parte del transmisor es la ubicación aguas arriba donde se coloca el pato de goma en el río, y la parte del receptor es donde termina el pato de goma aguas abajo. Una parte de tu código llama a métodos en el transmisor con los datos que quieres enviar, y otra parte revisa el extremo de recepción para los mensajes que llegan. Se dice que un canal está cerrado si se elimina la parte del transmisor o del receptor.

Aquí, trabajaremos hasta un programa que tenga un hilo para generar valores y enviarlos a través de un canal, y otro hilo que recibirá los valores y los imprimirá. Enviaremos valores simples entre hilos usando un canal para ilustrar la característica. Una vez que estés familiarizado con la técnica, podrías usar canales para cualquier hilo que necesite comunicarse con otro, como un sistema de chat o un sistema donde muchos hilos realizan partes de un cálculo y envían las partes a un hilo que agrega los resultados.

Primero, en la Lista 16-6, crearemos un canal pero no haremos nada con él. Tenga en cuenta que esto no se compilará todavía porque Rust no puede determinar qué tipo de valores queremos enviar a través del canal.

Nombre de archivo: src/main.rs

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}

Lista 16-6: Creando un canal y asignando las dos partes a tx y rx

Creamos un nuevo canal usando la función mpsc::channel; mpsc significa multiple producer, single consumer. En resumen, la forma en que la biblioteca estándar de Rust implementa los canales significa que un canal puede tener múltiples extremos de envío que producen valores pero solo un extremo de recepción que consume esos valores. Imagina múltiples arroyos que fluyen juntos en un gran río: todo lo que se envía por cualquiera de los arroyos terminará en un solo río al final. Empezaremos con un solo productor por ahora, pero agregaremos múltiples productores cuando tengamos este ejemplo funcionando.

La función mpsc::channel devuelve un par, cuyo primer elemento es el extremo de envío, el transmisor, y cuyo segundo elemento es el extremo de recepción, el receptor. Las abreviaturas tx y rx se usan tradicionalmente en muchos campos para transmisor y receptor, respectivamente, por lo que nombramos nuestras variables de esa manera para indicar cada extremo. Estamos usando una declaración let con un patrón que desestructura los pares; discutiremos el uso de patrones en declaraciones let y la desestructuración en el Capítulo 18. Por ahora, sabe que usar una declaración let de esta manera es un enfoque conveniente para extraer los fragmentos del par devuelto por mpsc::channel.

Movamos el extremo de transmisión a un hilo creado y que envíe una cadena para que el hilo creado se comunique con el hilo principal, como se muestra en la Lista 16-7. Esto es como poner un pato de goma en el río aguas arriba o enviar un mensaje de chat de un hilo a otro.

Nombre de archivo: src/main.rs

use std::sync::mpsc;
use std::thread;

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

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
}

Lista 16-7: Moviendo tx a un hilo creado y enviando "hi"

Una vez más, estamos usando thread::spawn para crear un nuevo hilo y luego usando move para mover tx a la clausura para que el hilo creado sea dueño de tx. El hilo creado necesita ser dueño del transmisor para poder enviar mensajes a través del canal.

El transmisor tiene un método send que toma el valor que queremos enviar. El método send devuelve un tipo Result<T, E>, por lo que si el receptor ya ha sido eliminado y no hay ningún lugar donde enviar un valor, la operación de envío devolverá un error. En este ejemplo, estamos llamando a unwrap para generar un error en caso de error. Pero en una aplicación real, lo manejaríamos adecuadamente: regrese al Capítulo 9 para revisar las estrategias para el manejo adecuado de errores.

En la Lista 16-8, obtendremos el valor del receptor en el hilo principal. Esto es como recuperar el pato de goma del agua al final del río o recibir un mensaje de chat.

Nombre de archivo: src/main.rs

use std::sync::mpsc;
use std::thread;

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

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {received}");
}

Lista 16-8: Recibiendo el valor "hi" en el hilo principal y lo imprimiendo

El receptor tiene dos métodos útiles: recv y try_recv. Estamos usando recv, abreviatura de receive, que bloqueará la ejecución del hilo principal y esperará hasta que se envíe un valor a través del canal. Una vez que se envía un valor, recv lo devolverá en un Result<T, E>. Cuando el transmisor se cierra, recv devolverá un error para indicar que ya no se enviarán más valores.

El método try_recv no bloquea, sino que devolverá inmediatamente un Result<T, E>: un valor Ok que contiene un mensaje si hay uno disponible y un valor Err si no hay mensajes en este momento. Usar try_recv es útil si este hilo tiene otras tareas que hacer mientras espera mensajes: podríamos escribir un bucle que llame a try_recv de vez en cuando, maneje un mensaje si hay uno disponible y, de lo contrario, haga otras tareas por un tiempo hasta comprobar de nuevo.

Hemos usado recv en este ejemplo por simplicidad; no tenemos ninguna otra tarea para que el hilo principal haga más que esperar mensajes, por lo que bloquear el hilo principal es apropiado.

Cuando ejecutamos el código de la Lista 16-8, veremos el valor impreso desde el hilo principal:

Got: hi

¡Perfecto!

Canales y transferencia de propiedad

Las reglas de propiedad juegan un papel fundamental en el envío de mensajes porque te ayudan a escribir código concurrente seguro. Prevenir errores en la programación concurrente es la ventaja de pensar en la propiedad en todo tu programa Rust. Hagamos un experimento para mostrar cómo los canales y la propiedad trabajan juntos para prevenir problemas: intentaremos usar un valor val en el hilo creado después de haberlo enviado a través del canal. Intenta compilar el código de la Lista 16-9 para ver por qué este código no está permitido.

Nombre de archivo: src/main.rs

use std::sync::mpsc;
use std::thread;

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

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
        println!("val is {val}");
    });

    let received = rx.recv().unwrap();
    println!("Got: {received}");
}

Lista 16-9: Intentando usar val después de haberlo enviado a través del canal

Aquí, intentamos imprimir val después de haberlo enviado a través del canal mediante tx.send. Permitir esto sería una mala idea: una vez que el valor ha sido enviado a otro hilo, ese hilo podría modificarlo o eliminarlo antes de que intentemos usar el valor nuevamente. Potencialmente, las modificaciones del otro hilo podrían causar errores o resultados inesperados debido a datos inconsistentes o inexistentes. Sin embargo, Rust nos da un error si intentamos compilar el código de la Lista 16-9:

error[E0382]: borrow of moved value: `val`
  --> src/main.rs:10:31
   |
8  |         let val = String::from("hi");
   |             --- move occurs because `val` has type `String`, which does
not implement the `Copy` trait
9  |         tx.send(val).unwrap();
   |                 --- value moved here
10 |         println!("val is {val}");
   |                           ^^^ value borrowed here after move

Nuestro error de concurrencia ha causado un error de compilación. La función send toma la propiedad de su parámetro, y cuando el valor se mueve, el receptor toma la propiedad de él. Esto nos impide usar accidentalmente el valor nuevamente después de enviarlo; el sistema de propiedad comprueba que todo está bien.

Enviando múltiples valores y viendo al receptor esperando

El código de la Lista 16-8 se compiló y ejecutó, pero no nos mostró claramente que dos hilos separados estaban comunicándose entre sí a través del canal. En la Lista 16-10 hicimos algunos cambios que demostrarán que el código de la Lista 16-8 está ejecutándose de manera concurrente: el hilo creado ahora enviará múltiples mensajes y pausará durante un segundo entre cada mensaje.

Nombre de archivo: src/main.rs

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

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

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {received}");
    }
}

Lista 16-10: Enviando múltiples mensajes y pausando entre cada uno

Esta vez, el hilo creado tiene un vector de cadenas que queremos enviar al hilo principal. Iteramos sobre ellos, enviando cada uno individualmente, y pausamos entre cada uno llamando a la función thread::sleep con un valor Duration de un segundo.

En el hilo principal, ya no estamos llamando explícitamente a la función recv: en su lugar, estamos tratando a rx como un iterador. Para cada valor recibido, lo imprimimos. Cuando el canal se cierra, la iteración terminará.

Al ejecutar el código de la Lista 16-10, deberías ver la siguiente salida con una pausa de un segundo entre cada línea:

Got: hi
Got: from
Got: the
Got: thread

Debido a que no tenemos ningún código que pause o retrase en el for loop del hilo principal, podemos decir que el hilo principal está esperando a recibir valores del hilo creado.

Creando múltiples productores mediante la clonación del transmisor

Antes mencionamos que mpsc era un acrónimo de multiple producer, single consumer. Vamos a poner en práctica mpsc y expandir el código de la Lista 16-10 para crear múltiples hilos que todos envíen valores al mismo receptor. Lo podemos hacer clonando el transmisor, como se muestra en la Lista 16-11.

Nombre de archivo: src/main.rs

--snip--

let (tx, rx) = mpsc::channel();

let tx1 = tx.clone();
thread::spawn(move || {
    let vals = vec![
        String::from("hi"),
        String::from("from"),
        String::from("the"),
        String::from("thread"),
    ];

    for val in vals {
        tx1.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

thread::spawn(move || {
    let vals = vec![
        String::from("more"),
        String::from("messages"),
        String::from("for"),
        String::from("you"),
    ];

    for val in vals {
        tx.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

for received in rx {
    println!("Got: {received}");
}

--snip--

Lista 16-11: Enviando múltiples mensajes de múltiples productores

Esta vez, antes de crear el primer hilo creado, llamamos a clone en el transmisor. Esto nos dará un nuevo transmisor que podemos pasar al primer hilo creado. Pasamos el transmisor original a un segundo hilo creado. Esto nos da dos hilos, cada uno enviando mensajes diferentes al mismo receptor.

Cuando ejecutes el código, tu salida debería verse así:

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

Podrías ver los valores en otro orden, dependiendo de tu sistema. Esto es lo que hace que la concurrencia sea interesante y difícil al mismo tiempo. Si experimentas con thread::sleep, dándole diferentes valores en los diferentes hilos, cada ejecución será más indeterminista y creará una salida diferente cada vez.

Ahora que hemos visto cómo funcionan los canales, veamos un método diferente de concurrencia.

Resumen

¡Felicitaciones! Has completado el laboratorio de Uso de paso de mensajes para transferir datos entre hilos. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.