Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Łączenie wszystkiego w całość: Futures, Zadania i Wątki

Jak widzieliśmy w Rozdziale 16, wątki stanowią jedno z podejść do współbieżności. W tym rozdziale poznaliśmy inne podejście: używanie async z futures i strumieniami. Jeśli zastanawiasz się, kiedy wybrać jedną metodę zamiast drugiej, odpowiedź brzmi: to zależy! I w wielu przypadkach wyborem nie są wątki lub async, ale raczej wątki i async.

Wiele systemów operacyjnych od dziesięcioleci dostarczało modele współbieżności oparte na wątkach, a wiele języków programowania je obsługuje. Jednak te modele mają swoje kompromisy. Na wielu systemach operacyjnych zużywają sporo pamięci na każdy wątek. Wątki są również opcją tylko wtedy, gdy system operacyjny i sprzęt je obsługują. W przeciwieństwie do popularnych komputerów stacjonarnych i mobilnych, niektóre systemy wbudowane w ogóle nie mają systemu operacyjnego, więc nie mają też wątków.

Model async zapewnia inny – i ostatecznie uzupełniający – zestaw kompromisów. W modelu async operacje współbieżne nie wymagają własnych wątków. Zamiast tego mogą działać na zadaniach, tak jak używaliśmy trpl::spawn_task do uruchomienia pracy z funkcji synchronicznej w sekcji strumieni. Zadanie jest podobne do wątku, ale zamiast być zarządzanym przez system operacyjny, jest zarządzane przez kod na poziomie biblioteki: środowisko uruchomieniowe.

Istnieje powód, dla którego API do uruchamiania wątków i uruchamiania zadań są tak podobne. Wątki działają jako granica dla zestawów operacji synchronicznych; współbieżność jest możliwa między wątkami. Zadania działają jako granica dla zestawów operacji asynchronicznych; współbieżność jest możliwa zarówno między, jak i wewnątrz zadań, ponieważ zadanie może przełączać się między futures w swoim ciele. Wreszcie, futures są najbardziej szczegółową jednostką współbieżności w Rust, a każda future może reprezentować drzewo innych futures. Środowisko uruchomieniowe – a konkretnie jego egzekutor – zarządza zadaniami, a zadania zarządzają futures. W tym względzie zadania są podobne do lekkich, zarządzanych przez środowisko uruchomieniowe wątków z dodatkowymi możliwościami wynikającymi z bycia zarządzanym przez środowisko uruchomieniowe, a nie przez system operacyjny.

Nie oznacza to, że zadania async są zawsze lepsze od wątków (lub odwrotnie). Współbieżność z wątkami jest pod pewnymi względami prostszym modelem programowania niż współbieżność z async. Może to być siła lub słabość. Wątki są nieco w stylu „odpal i zapomnij”; nie mają natywnego odpowiednika dla future, więc po prostu działają do końca, bez przerywania, chyba że przez sam system operacyjny.

Okazuje się, że wątki i zadania często bardzo dobrze współpracują, ponieważ zadania mogą (przynajmniej w niektórych środowiskach uruchomieniowych) być przenoszone między wątkami. W rzeczywistości, pod maską, środowisko uruchomieniowe, którego używaliśmy – w tym funkcje spawn_blocking i spawn_task – jest domyślnie wielowątkowe! Wiele środowisk uruchomieniowych stosuje podejście zwane work stealing (kradzieżą pracy) do transparentnego przenoszenia zadań między wątkami, w oparciu o bieżące wykorzystanie wątków, aby poprawić ogólną wydajność systemu. To podejście faktycznie wymaga wątków i zadań, a zatem i futures.

Zastanawiając się, którą metodę zastosować, rozważ te zasady:

  • Jeśli praca jest bardzo równoległa (czyli CPU-bound), taka jak przetwarzanie dużej ilości danych, gdzie każda część może być przetwarzana oddzielnie, wątki są lepszym wyborem.
  • Jeśli praca jest bardzo współbieżna (czyli I/O-bound), taka jak obsługa wiadomości z wielu różnych źródeł, które mogą przychodzić w różnych odstępach czasu lub z różnymi prędkościami, async jest lepszym wyborem.

AJeśli potrzebujesz zarówno równoległości, jak i współbieżności, nie musisz wybierać między wątkami a async. Możesz ich swobodnie używać razem, pozwalając każdemu odgrywać rolę, w której jest najlepszy. Na przykład, Lista 17-25 pokazuje dość powszechny przykład tego rodzaju połączenia w rzeczywistym kodzie Rust.

Nazwa pliku: src/main.rs
extern crate trpl; // for mdbook test

use std::{thread, time::Duration};

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

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::block_on(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}
Lista 17-25: Wysyłanie wiadomości z blokującym kodem w wątku i oczekiwanie na wiadomości w bloku async

Zaczynamy od utworzenia kanału async, a następnie uruchamiamy wątek, który przejmuje własność strony nadawcy kanału za pomocą słowa kluczowego move. W wątku wysyłamy liczby od 1 do 10, usypiając na sekundę między każdą z nich. Na koniec uruchamiamy future utworzoną za pomocą bloku async przekazanego do trpl::block_on, tak jak robiliśmy to w całym rozdziale. W tej future oczekujemy na te wiadomości, tak jak w innych przykładach przekazywania wiadomości, które widzieliśmy.

Powracając do scenariusza, od którego zaczęliśmy rozdział, wyobraź sobie uruchamianie zestawu zadań kodowania wideo przy użyciu dedykowanego wątku (ponieważ kodowanie wideo jest obciążające dla procesora), ale powiadamianie interfejsu użytkownika o zakończeniu tych operacji za pomocą kanału async. Istnieją niezliczone przykłady tego rodzaju kombinacji w rzeczywistych przypadkach użycia.

Podsumowanie

To nie ostatni raz, kiedy spotkasz się ze współbieżnością w tej książce. Projekt w Rozdziale 21 zastosuje te koncepcje w bardziej realistycznej sytuacji niż prostsze przykłady omówione tutaj i porówna bezpośrednio rozwiązywanie problemów za pomocą wątków kontra zadań i futures.

Niezależnie od tego, które z tych podejść wybierzesz, Rust daje ci narzędzia potrzebne do pisania bezpiecznego, szybkiego, współbieżnego kodu – czy to dla serwera WWW o wysokiej przepustowości, czy dla wbudowanego systemu operacyjnego.

Następnie omówimy idiomatyczne sposoby modelowania problemów i strukturyzowania rozwiązań w miarę wzrostu programów Rust. Ponadto omówimy, jak idiomy Rust odnoszą się do tych, które możesz znać z programowania zorientowanego obiektowo.