Usando hilos para ejecutar código simultáneamente

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 Threads to Run Code Simultaneously. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, exploraremos el concepto de hilos en la programación y cómo se pueden utilizar para ejecutar código simultáneamente, lo que mejora el rendimiento pero agrega complejidad y posibles problemas como condiciones de carrera, bloqueos mortales y errores difíciles de reproducir.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100437{{"Usando hilos para ejecutar código simultáneamente"}} rust/for_loop -.-> lab-100437{{"Usando hilos para ejecutar código simultáneamente"}} rust/function_syntax -.-> lab-100437{{"Usando hilos para ejecutar código simultáneamente"}} rust/expressions_statements -.-> lab-100437{{"Usando hilos para ejecutar código simultáneamente"}} rust/lifetime_specifiers -.-> lab-100437{{"Usando hilos para ejecutar código simultáneamente"}} rust/method_syntax -.-> lab-100437{{"Usando hilos para ejecutar código simultáneamente"}} end

Using Threads to Run Code Simultaneously

En la mayoría de los sistemas operativos actuales, el código de un programa ejecutado se ejecuta en un proceso, y el sistema operativo gestionará múltiples procesos a la vez. Dentro de un programa, también puede haber partes independientes que se ejecutan simultáneamente. Las características que ejecutan estas partes independientes se llaman hilos. Por ejemplo, un servidor web podría tener múltiples hilos para que pudiera responder a más de una solicitud al mismo tiempo.

Dividir el cálculo en su programa en múltiples hilos para ejecutar múltiples tareas al mismo tiempo puede mejorar el rendimiento, pero también agrega complejidad. Debido a que los hilos pueden ejecutarse simultáneamente, no hay ninguna garantía inherente sobre el orden en el que se ejecutarán las partes de su código en diferentes hilos. Esto puede dar lugar a problemas, como:

  • Condiciones de carrera, donde los hilos acceden a datos o recursos en un orden inconsistente
  • Bloqueos mortales, donde dos hilos se están esperando el uno al otro, impidiendo que ambos hilos continúen
  • Errores que solo ocurren en ciertas situaciones y son difíciles de reproducir y corregir de manera confiable

Rust intenta mitigar los efectos negativos de utilizar hilos, pero la programación en un contexto multihilo todavía requiere un pensamiento cuidadoso y una estructura de código diferente a la de los programas que se ejecutan en un solo hilo.

Los lenguajes de programación implementan los hilos de varias maneras diferentes, y muchos sistemas operativos proporcionan una API que el lenguaje puede llamar para crear nuevos hilos. La biblioteca estándar de Rust utiliza un modelo 1:1 de implementación de hilos, según el cual un programa utiliza un hilo del sistema operativo por cada hilo del lenguaje. Hay cajas que implementan otros modelos de subprocesamiento que realizan diferentes concesiones al modelo 1:1.

Creating a New Thread with spawn

Para crear un nuevo hilo, llamamos a la función thread::spawn y le pasamos una clausura (hablamos de clausuras en el Capítulo 13) que contiene el código que queremos ejecutar en el nuevo hilo. El ejemplo de la Lista 16-1 imprime algunos textos desde el hilo principal y otros textos desde un nuevo hilo.

Nombre de archivo: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

Lista 16-1: Creando un nuevo hilo para imprimir una cosa mientras el hilo principal imprime otra cosa

Tenga en cuenta que cuando el hilo principal de un programa Rust finaliza, todos los hilos creados se detienen, independientemente de si han terminado de ejecutarse o no. La salida de este programa puede ser un poco diferente cada vez, pero se verá similar a lo siguiente:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Las llamadas a thread::sleep forzaran a un hilo a detener su ejecución durante un corto período de tiempo, permitiendo que otro hilo se ejecute. Los hilos probablemente tomarán turnos, pero eso no está garantizado: depende de cómo su sistema operativo programa los hilos. En esta ejecución, el hilo principal imprimió primero, aunque la declaración de impresión del hilo creado aparece primero en el código. Y aunque le dijimos al hilo creado que imprimiera hasta que i es 9, solo llegó a 5 antes de que el hilo principal se detuviera.

Si ejecuta este código y solo ve la salida del hilo principal, o no ve ninguna superposición, intente aumentar los números en los rangos para crear más oportunidades para que el sistema operativo cambie entre los hilos.

Waiting for All Threads to Finish Using join Handles

El código de la Lista 16-1 no solo detiene el hilo creado prematuramente la mayoría de las veces debido a que el hilo principal finaliza, sino que también, dado que no hay garantía sobre el orden en el que se ejecutan los hilos, no podemos garantizar que el hilo creado se ejecute en absoluto.

Podemos solucionar el problema de que el hilo creado no se ejecute o que finalice prematuramente guardando el valor de retorno de thread::spawn en una variable. El tipo de retorno de thread::spawn es JoinHandle<T>. Un JoinHandle<T> es un valor propiedad que, cuando llamamos al método join sobre él, esperará a que su hilo termine. La Lista 16-2 muestra cómo usar el JoinHandle<T> del hilo que creamos en la Lista 16-1 y llamar a join para asegurarnos de que el hilo creado finalice antes de que main salga.

Nombre de archivo: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

Lista 16-2: Guardando un JoinHandle<T> de thread::spawn para garantizar que el hilo se ejecute hasta el final

Llamar a join en el manejador bloquea el hilo que está actualmente en ejecución hasta que el hilo representado por el manejador finaliza. Bloquear un hilo significa que se impide que ese hilo realice trabajo o salga. Debido a que hemos colocado la llamada a join después del bucle for del hilo principal, al ejecutar la Lista 16-2 se debería producir una salida similar a esta:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

Los dos hilos continúan alternando, pero el hilo principal espera debido a la llamada a handle.join() y no finaliza hasta que el hilo creado ha terminado.

Pero veamos qué pasa cuando en lugar de eso movemos handle.join() antes del bucle for en main, así:

Nombre de archivo: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

El hilo principal esperará a que el hilo creado termine y luego ejecutará su bucle for, por lo que la salida ya no se intercalará, como se muestra aquí:

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Detalles tan pequeños como dónde se llama a join pueden afectar a si tus hilos se ejecutan al mismo tiempo o no.

Using move Closures with Threads

A menudo usaremos la palabra clave move con las clausuras pasadas a thread::spawn porque entonces la clausura tomará la propiedad de los valores que utiliza del entorno, transferiendo así la propiedad de esos valores de un hilo a otro. En "Capturing the Environment with Closures", discutimos move en el contexto de las clausuras. Ahora nos centraremos más en la interacción entre move y thread::spawn.

Observa en la Lista 16-1 que la clausura que pasamos a thread::spawn no toma argumentos: no estamos usando ningún dato del hilo principal en el código del hilo creado. Para usar datos del hilo principal en el hilo creado, la clausura del hilo creado debe capturar los valores que necesita. La Lista 16-3 muestra un intento de crear un vector en el hilo principal y usarlo en el hilo creado. Sin embargo, esto todavía no funcionará, como verás enseguida.

Nombre de archivo: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Lista 16-3: Intentando usar un vector creado por el hilo principal en otro hilo

La clausura usa v, por lo que capturará v y lo hará parte del entorno de la clausura. Dado que thread::spawn ejecuta esta clausura en un nuevo hilo, deberíamos poder acceder a v dentro de ese nuevo hilo. Pero cuando compilamos este ejemplo, obtenemos el siguiente error:

error[E0373]: closure may outlive the current function, but it borrows `v`,
which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

Rust infiere cómo capturar v, y dado que println! solo necesita una referencia a v, la clausura intenta prestar v. Sin embargo, hay un problema: Rust no puede saber cuánto tiempo ejecutará el hilo creado, por lo que no sabe si la referencia a v siempre será válida.

La Lista 16-4 presenta un escenario en el que es más probable que haya una referencia a v que no sea válida.

Nombre de archivo: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    drop(v); // oh no!

    handle.join().unwrap();
}

Lista 16-4: Un hilo con una clausura que intenta capturar una referencia a v de un hilo principal que elimina v

Si Rust nos permitiera ejecutar este código, es posible que el hilo creado se colocara inmediatamente en segundo plano sin ejecutarse en absoluto. El hilo creado tiene una referencia a v dentro, pero el hilo principal elimina inmediatamente v, usando la función drop que discutimos en el Capítulo 15. Luego, cuando el hilo creado comienza a ejecutarse, v ya no es válido, por lo que una referencia a él también es inválida. ¡Oh no!

Para corregir el error del compilador en la Lista 16-3, podemos seguir el consejo del mensaje de error:

help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

Al agregar la palabra clave move antes de la clausura, forzamos a la clausura a tomar la propiedad de los valores que está usando en lugar de permitir que Rust infiera que debe prestar los valores. La modificación de la Lista 16-3 mostrada en la Lista 16-5 se compilará y ejecutará como queremos.

Nombre de archivo: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

Lista 16-5: Usando la palabra clave move para forzar a una clausura a tomar la propiedad de los valores que utiliza

Podríamos tentarnos a intentar lo mismo para corregir el código de la Lista 16-4 donde el hilo principal llamó a drop usando una clausura move. Sin embargo, esta corrección no funcionará porque lo que intenta hacer la Lista 16-4 está prohibido por una razón diferente. Si agregamos move a la clausura, moveríamos v al entorno de la clausura y ya no podríamos llamar a drop en él en el hilo principal. En su lugar, obtendríamos este error del compilador:

error[E0382]: use of moved value: `v`
  --> src/main.rs:10:10
   |
4  |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not
implement the `Copy` trait
5  |
6  |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
7  |         println!("Here's a vector: {:?}", v);
   |                                           - variable moved due to use in
closure
...
10 |     drop(v); // oh no!
   |          ^ value used here after move

¡Las reglas de propiedad de Rust nos han salvado de nuevo! Obtenimos un error del código de la Lista 16-3 porque Rust estaba siendo conservador y solo prestaba v para el hilo, lo que significaba que el hilo principal teóricamente podría invalidar la referencia del hilo creado. Al decirle a Rust que mueva la propiedad de v al hilo creado, estamos garantizando a Rust que el hilo principal ya no usará v. Si cambiamos la Lista 16-4 de la misma manera, entonces estamos violando las reglas de propiedad cuando intentamos usar v en el hilo principal. La palabra clave move anula el comportamiento conservador predeterminado de Rust de prestar; no nos permite violar las reglas de propiedad.

Ahora que hemos cubierto qué son los hilos y los métodos proporcionados por la API de hilos, echemos un vistazo a algunas situaciones en las que podemos usar hilos.

Resumen

¡Felicitaciones! Has completado el laboratorio de Uso de Hilos para Ejecutar Código Simultáneamente. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.