programowanieInżynier obliczeń równoległych (Rust)

Jak działa bezpieczna wielowątkowość w Rust: co oznaczają marker-traits Send i Sync, jak kontrolują one przekazywanie i współdzielenie danych między wątkami oraz jak programista może prawidłowo implementować (lub zabraniać) te traity dla własnych typów?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Z problemem bezpiecznego działania w środowiskach wielowątkowych programiści zmagają się od dawna, niestrudzenie borykając się z problemami wyścigów, niespójnych danych i wycieków pamięci. W Rust wprowadzono unikalne podejście z marker-traitami Send i Sync, aby zminimalizować te problemy już na etapie kompilacji.

Problem — brak kontroli dostępu do współdzielonych danych między wątkami, prowadzący do trudnych do zdiagnozowania błędów. W wielu językach odpowiedzialność spoczywa w pełni na programiście, w Rust kompilator sam sprawdza, co można przekazywać/współdzielić między wątkami.

Rozwiązanie: trait Send zapewnia możliwość bezpiecznego przekazywania obiektu z jednego wątku do drugiego. Sync — możliwość wspólnego dostępu do referencji do obiektu z różnych wątków. Prawie wszystkie typy standardowe w Rust automatycznie implementują te traity, a typy niestandardowe mogą je zaimplementować ręcznie lub zabraniać przez impl !Send lub impl !Sync w specyficznych przypadkach.

Przykład kodu:

use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } // counter zawsze będzie równy 10 bez wyścigów!

Najważniejsze cechy:

  • Send oznacza przekazanie własności obiektu między wątkami.
  • Sync oznacza bezpieczne współdzielenie obiektu przez referencje.
  • Implementacja/zabranianie traitów dla własnych typów pozwala kontrolować zachowanie na etapie kompilacji.

Pytania z podstępem.

Czy typ z niebezpiecznymi wskaźnikami może być Send lub Sync?

Nie, jeśli typ zawiera raw pointer lub zasoby bez gwarancji bezpieczeństwa wątków, nie implementuje tych traitów, lub programista musi je zaimplementować ręcznie z pełną odpowiedzialnością (zwykle z unsafe impl Send/Sync).

Czy Rc<T> i RefCell<T> są Send lub Sync?

Nie, Rc<T> i RefCell<T> są niebezpieczne do użytku wielowątkowego (ani Send, ani Sync). Do scenariuszy wielowątkowych używają Arc<T> oraz Mutex/RwLock.

Co się stanie, jeśli zmienna statyczna zawiera typ bez zaimplementowanego Sync?

Rust nie pozwoli, aby taka zmienna statyczna istniała: musi być Sync, w przeciwnym razie kompilator zgłosi błąd.

Typowe błędy i antywzorce

  • Użycie Rc<T> zamiast Arc<T> przy współdzieleniu z wielu wątków.
  • Projektowanie struktur z wewnętrznymi niebezpiecznymi wskaźnikami i automatyczne zaufanie do traitu Send.
  • Naruszanie inwariantów poprzez użycie unsafe impl Send/Sync bez ścisłej kontroli.

Przykład z życia

Negatywny przypadek

Młody programista umieszcza obiekt Rc w thread::spawn — kod kompiluje się tylko wtedy, gdy Rc nie jest przekazywane między wątkami. Przy próbie wyciągnięcia Rc z thread::spawn występuje błąd kompilacji, ponieważ Rc nie implementuje Send i nie jest chronione przed wyścigami.

Zalety:

  • Kompilator od razu zapobiega błędowi wyścigu danych.

Wady:

  • Jeśli nie znać różnicy między Rc a Arc, trudno zrozumieć błąd.

Pozytywny przypadek

Używane jest Arc+Mutex do wielowątkowego licznika, wszystkie wątki działają z tymi samymi danymi za pomocą interfejsu bezpiecznego dla wątków. Brak wyścigów, kod jest bezpieczny i odporny.

Zalety:

  • Brak wyścigów, bezpieczeństwo pamięci, używamy marker-traits do zarządzania zachowaniem.

Wady:

  • Mutex i Arc posiadają overhead, wymaga znajomości prymitywów bezpiecznych dla wątków.