0Pricing
Learn Rust Coding · Lekcja

Wątki zakresowe

Pożyczanie przez wątki

Wątki zakresowe to bezpłatna lekcja Learn Rust Coding na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Learn Rust Coding, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Learn Rust Coding zawiera 4 lekcji w sumie.

Problem z pożyczaniem w wątkach

Domknięcie przekazane do zwykłego thread::spawn musi mieć cechę 'static: nie może pożyczać zmiennych lokalnych, ponieważ wątek może działać dłużej niż funkcja, która jest ich właścicielem. Dlatego tak często spotykają Państwo move i Arc.

Wątki zakresowe rozwiązują ten problem, gwarantując zakończenie każdego wątku przed końcem zakresu, dzięki czemu pożyczanie danych lokalnych staje się bezpieczne.

Dlaczego spawn wymaga 'static

W przypadku thread::spawn utworzony wątek może nadal działać po zakończeniu działania funkcji main lub dowolnej innej funkcji. Gdyby pożyczał zmienną lokalną, mogłaby ona zostać usunięta, zanim wątek przestałby z niej korzystać. Rust zabrania tego już podczas kompilacji, zmuszając Państwa do przeniesienia danych będących własnością do domknięcia.

use std::thread;

fn main() {
    let nums = vec![1, 2, 3];
    // move transfers ownership into the thread
    let handle = thread::spawn(move || {
        println!("in thread: {:?}", nums);
    });
    handle.join().unwrap();
}

Wprowadzenie do thread::scope

Stabilne od wersji Rust 1.63, std::thread::scope tworzy zakres, w którym wątki mogą pożyczać zmienne lokalne. Zakres blokuje działanie do czasu zakończenia wszystkich znajdujących się w nim wątków, dlatego pożyczone odwołania nigdy nie stają się nieprawidłowe.

Wątek tworzy się za pomocą s.spawn(...), używając uchwytu zakresu s zamiast thread::spawn.

use std::thread;

fn main() {
    let data = vec![10, 20, 30];
    thread::scope(|s| {
        s.spawn(|| {
            println!("borrowed: {:?}", data);
        });
    });
    // data is still usable here
    println!("after scope: {:?}", data);
}

Pożyczanie bez move

Wewnątrz thread::scope można odczytywać zmienne lokalne przez odwołanie, bez używania move. Wiele wątków zakresowych może jednocześnie współdzielić niezmienne pożyczenie tych samych danych, tak jak w przypadku zwykłych odwołań.

use std::thread;

fn main() {
    let message = String::from("shared text");
    thread::scope(|s| {
        s.spawn(|| println!("thread 1 sees: {}", message));
        s.spawn(|| println!("thread 2 sees: {}", message));
    });
    println!("main still owns: {}", message);
}

Dzielenie pracy na fragmenty wycinka

Często stosowany wzorzec polega na podzieleniu wycinka i przydzieleniu każdemu wątkowi przetwarzania jednego fragmentu. Wątki zakresowe upraszczają to rozwiązanie, ponieważ każdy wątek może bezpośrednio pożyczyć część oryginalnego wycinka, bez konieczności klonowania.

use std::thread;

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];
    let (left, right) = numbers.split_at(3);
    thread::scope(|s| {
        s.spawn(|| {
            let sum: i32 = left.iter().sum();
            println!("left sum:  {}", sum);
        });
        s.spawn(|| {
            let sum: i32 = right.iter().sum();
            println!("right sum: {}", sum);
        });
    });
}

Zbieranie wartości zwracanych

Podobnie jak w przypadku zwykłych wątków, s.spawn zwraca obiekt ScopedJoinHandle. Należy wywołać .join(), aby otrzymać wartość zwróconą przez wątek. Można zebrać uchwyty, a następnie po utworzeniu wszystkich wątków wywołać na nich join, aby zgromadzić wyniki.

use std::thread;

fn main() {
    let inputs = [2, 4, 6];
    let mut handles = vec![];
    thread::scope(|s| {
        for &x in &inputs {
            handles.push(s.spawn(move || x * x));
        }
        let results: Vec<i32> = handles.into_iter()
            .map(|h| h.join().unwrap())
            .collect();
        println!("{:?}", results);
    });
}

Pożyczanie modyfikowalne wymaga ostrożności

Dwa wątki zakresowe nie mogą jednocześnie posiadać modyfikowalnych pożyczeń tych samych danych, ponieważ naruszałoby to reguły aliasowania w Rust. Do modyfikowania współdzielonych danych z wielu wątków nadal potrzebują Państwo obiektu Mutex, ale jeden wątek może uzyskać unikalne modyfikowalne pożyczenie rozłącznych części.

Poniżej każdy wątek modyfikuje osobną połowę tablicy za pomocą split_at_mut.

use std::thread;

fn main() {
    let mut data = [1, 2, 3, 4];
    let (a, b) = data.split_at_mut(2);
    thread::scope(|s| {
        s.spawn(|| { for x in a.iter_mut() { *x *= 10; } });
        s.spawn(|| { for x in b.iter_mut() { *x += 100; } });
    });
    println!("{:?}", data);
}

Zakres automatycznie dołącza wątki

Nie trzeba wywoływać join dla każdego wątku zakresowego. Gdy domknięcie scope zwróci wynik, Rust automatycznie dołącza wszystkie wątki, dla których nie wywołano jeszcze join, zanim wykona dalsze instrukcje. Dzięki temu pożyczane dane są zagwarantowane jako prawidłowe przez cały czas życia wątku.

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

fn main() {
    let label = String::from("task");
    thread::scope(|s| {
        s.spawn(|| {
            thread::sleep(Duration::from_millis(30));
            println!("{} done", label);
        });
        println!("spawned, scope will wait");
    });
    println!("all scoped threads finished");
}

Łączenie zakresu ze współdzieloną modyfikacją

Gdy wątki muszą modyfikować tę samą wartość, należy połączyć wątki zakresowe z obiektem Mutex. Obiekt Arc nie jest potrzebny, ponieważ zakres pozwala wątkom bezpośrednio pożyczać lokalny obiekt Mutex.

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    thread::scope(|s| {
        for _ in 0..5 {
            s.spawn(|| {
                let mut n = counter.lock().unwrap();
                *n += 1;
            });
        }
    });
    println!("counter = {}", *counter.lock().unwrap());
}

Zakresowe czy utworzone: kiedy czego używać

Wątków zakresowych należy używać, gdy praca ma określony zakres i kończy się wewnątrz funkcji, a także gdy chcą Państwo pożyczać dane ze stosu bez używania Arc lub klonowania.

thread::spawn należy używać, gdy wątek musi działać dłużej niż bieżąca funkcja albo przez cały czas życia programu. Wątki zakresowe nie mogą opuścić swojego zakresu.

Przykład równoległego mapowania

Połączmy wszystko: niewielkie równoległe mapowanie, które przekształca każdy element wektora w osobnym wątku, pożyczając dane wejściowe, a następnie zbiera wyniki w odpowiedniej kolejności.

use std::thread;

fn parallel_double(items: &[i32]) -> Vec<i32> {
    let mut handles = Vec::new();
    let mut out = Vec::new();
    thread::scope(|s| {
        for &x in items {
            handles.push(s.spawn(move || x * 2));
        }
        for h in handles {
            out.push(h.join().unwrap());
        }
    });
    out
}

fn main() {
    let nums = vec![1, 2, 3, 4];
    println!("{:?}", parallel_double(&nums));
}

Szybki test

Sprawdź swoją wiedzę o wątkach zakresowych.

Podsumowanie

Dowiedzieli się Państwo o wątkach zakresowych:

  • thread::spawn wymaga domknięć z cechą 'static, a wątki zakresowe nie.
  • thread::scope pozwala wątkom bezpiecznie pożyczać zmienne lokalne.
  • Zakres automatycznie dołącza wszystkie wątki przed zwróceniem wyniku.
  • Współdzielenie modyfikowalne nadal wymaga obiektu Mutex, ale wewnątrz zakresu nie jest potrzebny Arc.
  • Wątków zakresowych należy używać do równoległego przetwarzania o określonym zakresie, lokalnego dla funkcji.

Często zadawane pytania

Czy lekcja „Wątki zakresowe” jest bezpłatna?

Tak — pełny tekst „Wątki zakresowe” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Learn Rust Coding, przejdź na CoddyKit PRO. Kurs Learn Rust Coding zawiera 4 lekcji w sumie.

Co nauczysz się w „Wątki zakresowe”?

Pożyczanie przez wątki Ćwiczysz Learn Rust Coding z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Learn Rust Coding?

Nie wymagamy żadnego doświadczenia. Learn Rust Coding w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Wątki zakresowe”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Learn Rust Coding?

Tak. Każda lekcja Learn Rust Coding zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Kanały mpsc
  2. Współdzielenie stanu za pomocą Arc/Mutex
  3. Wątki zakresowe
  4. Kanały Crossbeam
← Powrót do Learn Rust Coding