Verwenden von Threads, um Code gleichzeitig auszuführen

RustRustBeginner
Jetzt üben

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

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Using Threads to Run Code Simultaneously. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir das Konzept von Threads in der Programmierung erkunden und wie sie verwendet werden können, um Code gleichzeitig auszuführen, was die Leistung verbessert, aber auch Komplexität und potenzielle Probleme wie Wettlaufbedingungen, Deadlocks und schwer zu reproduzierende Fehler hinzufügt.


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{{"Verwenden von Threads, um Code gleichzeitig auszuführen"}} rust/for_loop -.-> lab-100437{{"Verwenden von Threads, um Code gleichzeitig auszuführen"}} rust/function_syntax -.-> lab-100437{{"Verwenden von Threads, um Code gleichzeitig auszuführen"}} rust/expressions_statements -.-> lab-100437{{"Verwenden von Threads, um Code gleichzeitig auszuführen"}} rust/lifetime_specifiers -.-> lab-100437{{"Verwenden von Threads, um Code gleichzeitig auszuführen"}} rust/method_syntax -.-> lab-100437{{"Verwenden von Threads, um Code gleichzeitig auszuführen"}} end

Verwenden von Threads, um Code gleichzeitig auszuführen

In den meisten aktuellen Betriebssystemen wird der Code eines ausgeführten Programms in einem Prozess ausgeführt, und das Betriebssystem verwaltet mehrere Prozesse gleichzeitig. Innerhalb eines Programms können Sie auch unabhängige Teile haben, die gleichzeitig ausgeführt werden. Die Funktionen, die diese unabhängigen Teile ausführen, werden Threads genannt. Beispielsweise könnte ein Webserver mehrere Threads haben, sodass er gleichzeitig auf mehrere Anfragen reagieren kann.

Das Aufteilen der Berechnung in Ihrem Programm in mehrere Threads, um mehrere Aufgaben gleichzeitig auszuführen, kann die Leistung verbessern, aber es fügt auch Komplexität hinzu. Da Threads gleichzeitig ausgeführt werden können, gibt es keine inhärente Garantie darüber, in welcher Reihenfolge die Teile Ihres Codes auf verschiedenen Threads ausgeführt werden. Dies kann zu Problemen führen, wie:

  • Wettlaufbedingungen, bei denen Threads Daten oder Ressourcen in einer inkonsistenten Reihenfolge zugreifen
  • Deadlocks, bei denen zwei Threads aufeinander warten und dadurch verhindern, dass beide Threads fortfahren
  • Fehler, die nur in bestimmten Situationen auftreten und schwer zu reproduzieren und zu beheben sind

Rust versucht, die negativen Auswirkungen der Verwendung von Threads zu mildern, aber das Programmieren in einem multithreaded Kontext erfordert dennoch sorgfältiges Denken und erfordert eine Code-Struktur, die sich von der in Programmen unterscheidet, die in einem einzelnen Thread ausgeführt werden.

Programmiersprachen implementieren Threads auf verschiedene Weise, und viele Betriebssysteme bieten eine API, die die Sprache aufrufen kann, um neue Threads zu erstellen. Die Rust-Standardbibliothek verwendet ein 1:1-Modell der Thread-Implementierung, bei dem ein Programm pro einem Sprach-Thread einen Betriebssystem-Thread verwendet. Es gibt Crates, die andere Modelle der Threading implementieren, die andere Kompromisse gegenüber dem 1:1-Modell machen.

Erstellen eines neuen Threads mit spawn

Um einen neuen Thread zu erstellen, rufen wir die thread::spawn-Funktion auf und übergeben ihr eine Closure (wir haben uns in Kapitel 13 mit Closures beschäftigt), die den Code enthält, den wir in dem neuen Thread ausführen möchten. Das Beispiel in Listing 16-1 druckt einige Text aus einem Hauptthread und anderen Text aus einem neuen Thread.

Dateiname: 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));
    }
}

Listing 16-1: Erstellen eines neuen Threads, um etwas zu drucken, während der Hauptthread etwas anderes druckt

Beachten Sie, dass wenn der Hauptthread eines Rust-Programms abgeschlossen ist, alle erzeugten Threads beendet werden, unabhängig davon, ob sie fertig ausgeführt sind oder nicht. Die Ausgabe dieses Programms kann jedes Mal etwas anders sein, aber es wird ähnlich wie folgendes aussehen:

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!

Die Aufrufe von thread::sleep zwingen einen Thread, seine Ausführung für eine kurze Zeit zu stoppen, um einen anderen Thread laufen zu lassen. Die Threads werden wahrscheinlich abwechselnd ausgeführt, aber das ist nicht gewährleistet: Es hängt davon ab, wie Ihr Betriebssystem die Threads planst. In dieser Ausführung hat der Hauptthread zuerst gedruckt, obwohl der Print-Befehl aus dem erzeugten Thread zuerst im Code erscheint. Und obwohl wir dem erzeugten Thread gesagt haben, bis i 9 zu drucken, hat er erst bis 5 gedruckt, bevor der Hauptthread beendet wurde.

Wenn Sie diesen Code ausführen und nur die Ausgabe des Hauptthreads sehen oder keine Überlappung sehen, versuchen Sie, die Zahlen in den Bereichen zu erhöhen, um mehr Möglichkeiten für das Betriebssystem zu schaffen, zwischen den Threads zu wechseln.

Warten auf das Ende aller Threads mit join Handles

Der Code in Listing 16-1 stoppt den erzeugten Thread nicht nur meistens vorzeitig aufgrund des Endens des Hauptthreads, sondern auch, weil es keine Garantie gibt, in welcher Reihenfolge die Threads ausgeführt werden, können wir auch nicht garantieren, dass der erzeugte Thread überhaupt ausgeführt wird!

Wir können das Problem des nicht ausgeführten oder vorzeitigen Endens des erzeugten Threads beheben, indem wir den Rückgabewert von thread::spawn in einer Variable speichern. Der Rückgabetyp von thread::spawn ist JoinHandle<T>. Ein JoinHandle<T> ist ein eigenes Objekt, das, wenn wir die join-Methode darauf aufrufen, auf das Ende seines Threads warten wird. Listing 16-2 zeigt, wie man das JoinHandle<T> des Threads, den wir in Listing 16-1 erstellt haben, verwendet und join aufruft, um sicherzustellen, dass der erzeugte Thread vor dem Verlassen von main abgeschlossen ist.

Dateiname: 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();
}

Listing 16-2: Speichern eines JoinHandle<T> von thread::spawn, um sicherzustellen, dass der Thread bis zum Abschluss ausgeführt wird

Das Aufrufen von join auf dem Handle blockiert den derzeit ausgeführten Thread, bis der von dem Handle dargestellte Thread terminiert. Ein Thread blockieren bedeutet, dass dieser Thread daran gehindert wird, Arbeit auszuführen oder zu beenden. Da wir den Aufruf von join nach der for-Schleife des Hauptthreads platziert haben, sollte das Ausführen von Listing 16-2 eine Ausgabe ähnlich dieser erzeugen:

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!

Die beiden Threads wechseln weiterhin abwechselnd, aber der Hauptthread wartet aufgrund des Aufrufs von handle.join() und endet erst, wenn der erzeugte Thread fertig ist.

Aber sehen wir uns an, was passiert, wenn wir handle.join() stattdessen vor der for-Schleife in main verschieben, wie folgt:

Dateiname: 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));
    }
}

Der Hauptthread wird auf das Ende des erzeugten Threads warten und dann seine for-Schleife ausführen, sodass die Ausgabe nicht mehr verzahnt ist, wie hier gezeigt:

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!

Kleine Details wie der Ort, an dem join aufgerufen wird, können sich darauf auswirken, ob Ihre Threads gleichzeitig ausgeführt werden.

Verwenden von move Closures mit Threads

Wir werden oft das move-Schlüsselwort mit Closures verwenden, die an thread::spawn übergeben werden, weil das Closure dann die Werte, die es aus der Umgebung verwendet, in Besitz nimmt und so die Besitzübertragung dieser Werte von einem Thread an einen anderen ermöglicht. In "Capturing the Environment with Closures" haben wir move im Zusammenhang mit Closures diskutiert. Jetzt werden wir uns stärker auf die Wechselwirkung zwischen move und thread::spawn konzentrieren.

Beachten Sie in Listing 16-1, dass das Closure, das wir an thread::spawn übergeben, keine Argumente nimmt: Wir verwenden keine Daten aus dem Hauptthread im Code des erzeugten Threads. Um Daten aus dem Hauptthread im erzeugten Thread zu verwenden, muss das Closure des erzeugten Threads die Werte, die es benötigt, aufnehmen. Listing 16-3 zeigt einen Versuch, einen Vektor im Hauptthread zu erstellen und ihn im erzeugten Thread zu verwenden. Dies wird jedoch noch nicht funktionieren, wie Sie gleich sehen werden.

Dateiname: 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();
}

Listing 16-3: Versuch, einen von Hauptthread erstellten Vektor in einem anderen Thread zu verwenden

Das Closure verwendet v, also wird v aufgenommen und zu Teil der Umgebung des Closures. Da thread::spawn dieses Closure in einem neuen Thread ausführt, sollten wir in diesem neuen Thread auf v zugreifen können. Wenn wir jedoch dieses Beispiel kompilieren, erhalten wir folgenden Fehler:

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 schließt daraus, wie v aufgenommen werden soll, und da println! nur eine Referenz auf v benötigt, versucht das Closure, v zu entleihen. Es gibt jedoch ein Problem: Rust kann nicht wissen, wie lange der erzeugte Thread läuft, daher weiß es auch nicht, ob die Referenz auf v immer gültig sein wird.

Listing 16-4 stellt einen Szenario vor, in dem es wahrscheinlicher ist, dass eine Referenz auf v ungültig wird.

Dateiname: 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();
}

Listing 16-4: Ein Thread mit einem Closure, das versucht, eine Referenz auf v aus einem Hauptthread zu erfassen, der v löscht

Wenn Rust uns erlaubte, diesen Code auszuführen, gäbe es die Möglichkeit, dass der erzeugte Thread sofort in den Hintergrund gesetzt wird und überhaupt nicht ausgeführt wird. Der erzeugte Thread hat eine Referenz auf v drin, aber der Hauptthread löst sofort v mit der drop-Funktion, die wir in Kapitel 15 diskutiert haben. Dann, wenn der erzeugte Thread beginnt, auszuführen, ist v nicht mehr gültig, daher ist auch eine Referenz darauf ungültig. Oh nein!

Um den Compilerfehler in Listing 16-3 zu beheben, können wir den Tipp aus der Fehlermeldung verwenden:

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 || {
  |                                ++++

Indem wir das move-Schlüsselwort vor das Closure hinzufügen, zwingen wir das Closure, die Werte, die es verwendet, in Besitz zu nehmen, anstatt Rust zu erlauben, zu schließen, dass es die Werte entleihen sollte. Die in Listing 16-5 gezeigte Änderung von Listing 16-3 wird wie gewünscht kompilieren und ausgeführt.

Dateiname: 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();
}

Listing 16-5: Verwenden des move-Schlüsselworts, um ein Closure dazu zu zwingen, die Werte, die es verwendet, in Besitz zu nehmen

Wir könnten versucht sein, das gleiche zu tun, um den Code in Listing 16-4 zu beheben, in dem der Hauptthread drop aufgerufen hat, indem wir ein move-Closure verwenden. Dieser Fix funktioniert jedoch nicht, weil das, was Listing 16-4 versucht zu tun, aus einem anderen Grund nicht zugelassen wird. Wenn wir move zum Closure hinzufügen würden, würden wir v in die Umgebung des Closures verschieben, und wir könnten es dann nicht mehr in dem Hauptthread mit drop aufrufen. Stattdessen würden wir folgenden Compilerfehler erhalten:

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

Rusts Besitzregeln haben uns wieder gerettet! Wir haben einen Fehler aus dem Code in Listing 16-3 erhalten, weil Rust konservativ ist und nur v für den Thread entleiht, was bedeutet, dass der Hauptthread theoretisch die Referenz des erzeugten Threads ungültig machen könnte. Indem wir Rust sagen, die Besitzübertragung von v an den erzeugten Thread zu machen, gewährleisten wir Rust, dass der Hauptthread v nicht mehr verwenden wird. Wenn wir Listing 16-4 auf die gleiche Weise ändern, verletzen wir dann die Besitzregeln, wenn wir versuchen, v im Hauptthread zu verwenden. Das move-Schlüsselwort überschreibt Rusts konservativen Standard der Entleihe; es lässt uns die Besitzregeln nicht verletzen.

Jetzt, nachdem wir gesehen haben, was Threads sind und welche Methoden die Thread-API zur Verfügung stellt, schauen wir uns einige Situationen an, in denen wir Threads verwenden können.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Labor "Using Threads to Run Code Simultaneously" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.