Powrót do strony głównej

Arc i Mutex dla równoległego stanu w Rust

Artykuł omawia wykorzystanie Arc i Mutex do bezpiecznego dostępu do współdzielonego stanu w wielowątkowych aplikacjach Rust. Przykład licznika, porównanie inteligentnych wskaźników i rekomendacje optymalizacyjne. Nadaje się dla middle/senior developerów.

Równoległy stan w Rust: Arc + Mutex w praktyce
Advertisement 728x90

Bezpieczny dostęp do stanu współdzielonego w wielowątkowych aplikacjach w Rust

Współdzielony dostęp do pamięci między wątkami bez zabezpieczeń prowadzi do stanu wyścigu, gdy jednoczesne odczyty i zapisy zniekształcają dane. Nawet przy synchronizacji pojawiają się blokady spowalniające wykonanie. Debugowanie takiego kodu jest utrudnione, choć Rust zapobiega wielu błędom na etapie kompilacji.

W systemach o wysokim obciążeniu, takich jak liczniki czy pamięć podręczna, współdzielony dostęp jest uzasadniony, jeśli zastosuje się odpowiednie prymitywy synchronizacji.

Arc i Mutex do bezpiecznej wymiany danych między wątkami

Do bezpiecznego przesyłania danych między wątkami łączy się Arc<T> i Mutex<T>. Arc<T> zapewnia atomowe zliczanie referencji z obsługą Send i Sync, umożliwiając wielokrotne posiadanie w środowisku wielowątkowym. Mutex<T> gwarantuje wzajemne wykluczenie: tylko jeden wątek uzyskuje dostęp w danym momencie.

Google AdInline article slot

Arc jest klonowany dla każdego wątku za pomocą Arc::clone, a dostęp otwiera się przez lock(), który zwraca MutexGuard. Metoda lock() może zakończyć się paniką przy zatrutym mutexie, dlatego w produkcji stosuje się obsługę błędów zamiast unwrap.

Przykład implementacji licznika

use std::thread;
use std::time::Duration;
use std::sync::{Arc, Mutex};

struct Counter {
    value: i64
}

fn main() {
    let counter = Arc::new(Mutex::new(Counter {value: 0}));

    let increment = Arc::clone(&counter);
    let handler1 = thread::spawn(move || {
        for _ in 0..10 {
            let mut guard = increment.lock().unwrap();
            guard.value += 1;
            println!("inc: {}", guard.value);
            thread::sleep(Duration::from_millis(100));
        }
    });

    let reader = Arc::clone(&counter);
    let handler2 = thread::spawn(move || {
        for _ in 0..15 {
            let guard = reader.lock().unwrap();
            println!("read: {}", guard.value);
            thread::sleep(Duration::from_millis(180));
        }
    });

    handler1.join().unwrap();
    handler2.join().unwrap();

    let final_guard = counter.lock().unwrap();
    println!("final: {}", final_guard.value);
}

Ten kod demonstruje inkrementację w jednym wątku i odczyt w drugim. Końcowa wartość zawsze wynosi 10, bez wyścigów danych.

Porównanie kluczowych inteligentnych wskaźników

Rust oferuje kilka inteligentnych wskaźników do zarządzania pamięcią. Wybór zależy od wymagań dotyczących bezpieczeństwa wątków i posiadania:

Google AdInline article slot
  • Box<T>: Pojedyncze posiadanie na stercie, nadaje się dla typów o nieznanym rozmiarze, nie Send + Sync.
  • Rc<T>: Wielokrotne posiadanie w jednym wątku, nie jest bezpieczny dla wątków.
  • Arc<T>: Wielokrotne posiadanie między wątkami, Send + Sync, obciążenie operacjami atomowymi.
  • RefCell<T>: Wewnętrzna zmienność z runtime'ową kontrolą pożyczania, jednowątkowy.
  • Cell<T>: Lekka zmienność dla typów Copy, bez panik.
  • Mutex<T>: Blokada dla wyłącznego dostępu, ryzyko zakleszczeń.
  • RwLock<T>: Optymalizacja dla scenariuszy z przewagą odczytów.
  • Weak<T>: Słabe referencje do rozbijania cykli w Rc/Arc.
  • ManuallyDrop<T>: Ręczne zarządzanie destruktorem, niebezpieczne.

Arc<T> jest unikalny w std dla bezpiecznego wielokrotnego posiadania między wątkami.

Alternatywy dla Mutex w optymalizacji

W scenariuszach z częstymi odczytami rozważ RwLock<T>: wielu czytelników działa równolegle, pisarze wyłącznie. Wydajność jest wyższa przy stosunku czytelników/pisarzy > 1.

Dla dostępu bez blokowania użyj atomików (std::sync::atomic), jeśli dane to prymitywy jak AtomicI64. Unikają one całkowicie blokad.

Google AdInline article slot

Co jest ważne

  • Współdzielony dostęp jest uzasadniony tylko w systemach o wysokim obciążeniu ze wspólnymi danymi, w przeciwnym razie preferuj przekazywanie posiadania.
  • Zawsze łącz Arc<T> z Mutex<T> lub RwLock<T> dla bezpieczeństwa wątków.
  • Unikaj unwrap() na lock(): obsługuj PoisonError.
  • Arc<T> niesie obciążenie operacjami atomowymi — mierz w benchmarkach.
  • Dla prostych przypadków używaj kanałów (mpsc) zamiast stanu współdzielonego.

— Editorial Team

Advertisement 728x90

Czytaj dalej