Arc::make_mut stara się zapewnić mutowalny dostęp do wewnętrznych danych, najpierw weryfikując, że Arc posiada jedyną silną referencję do alokacji. Wykonuje to sprawdzenie, używając atomowego odczytu z porządkiem Acquire na liczniku silnych referencji. Jeśli liczba jest dokładnie równa jeden, operacja przechodzi do zwrócenia mutowalnej referencji; w przeciwnym razie klonuje wewnętrzne dane i aktualizuje Arc, aby wskazywał na nową alokację.
use std::sync::Arc; let mut data = Arc::new(5); *Arc::make_mut(&mut data) += 1; // Klonuje tylko, jeśli jest współdzielony
Para Acquire/Release jest niezbędna, ponieważ gdy inny wątek zwalnia swój Arc, wykonuje dekrementację Release na liczniku. Ładowanie Acquire w make_mut zapewnia, że wszystkie zapisy pamięci wykonane przez wątek zwalniający przed dekrementacją są widoczne dla bieżącego wątku, zapobiegając wyścigom danych na wewnętrznych danych.
Przemyśl serwis agregacji metryk o wysokiej przepustowości, gdzie aktualizacje konfiguracji są propagowane za pomocą Arc<Config>. Tysiące wątków trzyma referencje do odczytu bieżących ustawień, ale wątek administratora okresowo musi dostosować progi bez ponownego uruchamiania usługi.
Naivna metoda polega na owinięciu Config w RwLock i zablokowaniu go dla każdego odczytu lub klonowaniu całej struktury dla każdej drobnej aktualizacji, niezależnie od dzielenia. Pierwsze rozwiązanie cierpi z powodu wymiany linii pamięci podręcznej i narzutu związane z blokadą, podczas gdy drugie marnuje pamięć i cykle CPU na zbędne alokacje, gdy konfiguracja jest w rzeczywistości unikalna.
Alternatywą jest użycie AtomicPtr z wskaźnikami hazardowymi do aktualizacji bez zablokowania, ale to wymaga skomplikowanego ręcznego zarządzania pamięcią i jest podatne na błędy. Inną opcją jest użycie RwLock<Arc<Config>>, co pozwala na atomowe wymiany samego wskaźnika, ale to dodaje dodatkową indykację i blokadę dla wymiany wskaźnika.
Zespół wybrał Arc::make_mut, ponieważ optymalizuje dla najczęściej występującego przypadku: jeśli żaden inny wątek nie trzyma referencji (silny licznik wynosi 1), wątek administratora modyfikuje dane na miejscu bez alokacji. Jeśli konfiguracja jest współdzielona, jest ona przejrzysto klonowana. Wymaga to ścisłych semantyki Acquire/Release, aby zapewnić, że gdy ostatni inny czytelnik zwalnia swoją Arc (używając Release), kolejna kontrola wątku administratora (używając Acquire) widzi wszystkie wcześniejsze zapisy do konfiguracji, zapobiegając zakłóconym odczytom. Efekt był 40% redukcji opóźnienia w aktualizacjach konfiguracji przy niskiej rywalizacji.
Dlaczego porządek Relaxed nie może być użyty do sprawdzenia liczby referencji w Arc::make_mut?
Operacje Relaxed nie zapewniają żadnych gwarancji zdarzeń. Jeśli make_mut użyłby Relaxed, aby sprawdzić, czy licznik silny wynosi 1, mógłby zaobserwować dekrement liczby z innego wątku, zanim zaobserwuje zapisy tego wątku do wewnętrznych danych. To pozwoliłoby bieżącemu wątkowi modyfikować dane, podczas gdy inny wątek nadal logicznie je odczytuje, co prowadzi do wyścigu danych. Acquire zapewnia, że gdy widzimy, że licznik osiąga 1 (zsynchronizowany poprzez Release w zwolnieniu innego wątku), widzimy również wszystkie wcześniejsze zapisy do danych.
Czym różni się zachowanie Arc::make_mut od ręcznego klonowania Arc za pomocą .clone() a następnie modyfikacji?
Ręczne klonowanie tworzy nowy Arc wskazujący na tę samą alokację, zwiększając silny licznik do co najmniej 2. Nie można uzyskać mutowalnego dostępu do wewnętrznych danych poprzez ten nowy Arc, ponieważ Arc zapewnia tylko niemutowalne dzielenie. Arc::make_mut jest wyjątkowy, ponieważ sprawdza, czy licznik wynosi 1; jeśli tak, zapewnia &mut T dla istniejącej alokacji. Jeśli nie, klonuje dane do nowej alokacji z licznikiem 1, zapewniając, że oryginalne współdzielone dane pozostają niemutowalne, a Ty uzyskujesz unikalne posiadanie nowej kopii.
Jak słabe wskaźniki (Arc::downgrade) wpływają na gwarancję unikalności Arc::make_mut?
Słabe wskaźniki nie uczestniczą w liczniku silnych referencji. Arc::make_mut sprawdza tylko silny licznik, ignorując słabe referencje. Jednak słabe wskaźniki mogą być przekształcane w silne, jeśli alokacja nadal istnieje. Jeśli make_mut proceduje z mutacją na miejscu (silny licznik wynosi 1), a inny wątek następnie przekształca słaby wskaźnik, to przekształcenie utworzy nowy Arc wskazujący na te same zmodyfikowane dane. To jest bezpieczne, ponieważ przekształcenie zdarza się po mutacji, a model pamięci Rust gwarantuje, że przekształcony wskaźnik widzi w pełni zmodyfikowaną wartość. Licznik słaby nie zapobiega mutacji, ale utrzymuje alokację na żywo, nawet jeśli wszystkie silne referencje są tymczasowo zwalniane.