programowanieProgramista Backend

Jakie sposoby synchronizacji wątków oferuje standardowa biblioteka Rust? Jak wybierać między nimi i jakie "cienkie miejsca" należy koniecznie wziąć pod uwagę?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

W standardowej bibliotece Rust znajdują się podstawowe prymitywy synchronizacji do bezpiecznej pracy z wielowątkowością:

  • Mutex — zapewnia wzajemne wykluczanie dostępu do danych z kilku wątków;
  • RwLock — umożliwia jednoczesne kilku czytelnikom (read), ale tylko jednemu pisarzowi (write);
  • Condvar — prymityw warunku dla organizacji budzenia wątków na podstawie zdarzenia;
  • Typy Atomiczne (AtomicBool, AtomicUsize itd.) — operacje odczytu/zapisu bez blokad, na poziomie sprzętowym;
  • Arc (Atomic Reference Counted) — zliczanie referencji z bezpieczeństwem wątkowym, do współdzielenia obiektami.

Wybór:

  • Jeśli wymagana jest tylko jednoczesna lektura — użyj RwLock (wydajniejsze niż Mutex).
  • Dla prostego, synchronizowanego dostępu jednego wątku — Mutex.
  • Do synchronizacji na podstawie sygnału (na przykład „czekać na nowy element”) — Condvar.
  • Do liczników/flagi atomowych — Atomic.
  • Do współdzielenia (wielowątkowego) — Arc.

Przykład:

use std::sync::{Arc, Mutex}; use std::thread; fn main() { 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(); } println!("Wynik: {}", *counter.lock().unwrap()); }

Pytanie z podstępem

Pytanie: Czy Rust gwarantuje, że użycie Mutex<T> całkowicie eliminuje deadlocki dzięki sprawdzeniu na etapie kompilacji?

Odpowiedź: Nie. Rust zapewnia bezpieczny dostęp do danych poprzez własność i borrow-checker, ale nie chroni przed deadlockami na poziomie języka. Martwe blokady powstają czysto logicznie w wyniku naruszenia porządku przechwytywania wielu Mutex lub ich rekursywnego przechwytywania. Przykład:

use std::sync::Mutex; let lock1 = Mutex::new(0); let lock2 = Mutex::new(0); // Wątek 1: lock1 -> lock2, Wątek 2: lock2 -> lock1 ⇒ deadlock

Historia

Projekt przetwarzania strumieniowego danych doświadczył przypadkowych "zawieszeń". Okazało się, że programiści używali zagnieżdżonych Mutex (Mutex wewnątrz Mutex) bez ścisłego porządku przechwytywania, co prowadziło do wzajemnych blokad (deadlock), które można było usunąć tylko poprzez wymuszone zakończenie procesu.

Historia

W dużym serwisie masowo migrowano z Mutex<Option<T>> na RwLock<T>, nie wzięto pod uwagę, że blokada zapisu może być dłuższa niż blokada odczytu. W okresie szczytowego obciążenia spowodowało to opóźnienia w przetwarzaniu do kilkudziesięciu sekund z powodu kolejek na zapis.

Historia

Programiści próbowali oszczędzać na wątkach, "popychali" Arc<Mutex<_>> do setek wątków. Z powodu niuansów pracy planisty i ponownego wykorzystania mutex blokady pojawiały się zdumiewające wzajemne oczekiwania — wydajność spadła pięciokrotnie!