Arc::make_mut tente de fournir un accès mutable aux données internes en vérifiant d'abord que l'Arc détient la seule référence forte à l'allocation. Il effectue cette vérification en utilisant un chargement atomique avec un ordre Acquire sur le compte de références fortes. Si le compte est exactement un, l'opération continue pour retourner une référence mutable ; sinon, elle clone les données internes et met à jour l'Arc pour pointer vers la nouvelle allocation.
use std::sync::Arc; let mut data = Arc::new(5); *Arc::make_mut(&mut data) += 1; // Clones only if shared
La paire Acquire/Release est essentielle car lorsqu'un autre thread supprime son Arc, il effectue un décrément Release sur le compte. Le chargement Acquire dans make_mut garantit que toutes les écritures en mémoire effectuées par le thread qui supprime avant le décrément sont visibles pour le thread actuel, empêchant les courses de données sur les données internes.
Considérez un service d'agrégation de métriques à haut débit où les mises à jour de configuration sont propagées via Arc<Config>. Des milliers de threads détiennent des références pour lire les paramètres actuels, mais le thread administrateur doit périodiquement ajuster les seuils sans redémarrer le service.
L'approche naïve consiste à envelopper le Config dans un RwLock et à le verrouiller pour chaque lecture, ou à cloner toute la structure pour chaque mise à jour mineure indépendamment du partage. La première solution souffre de rebonds de lignes de cache et d'un coût de verrouillage, tandis que la seconde gaspille de la mémoire et des cycles CPU sur des allocations redondantes lorsque la configuration est en fait unique.
Une alternative consiste à utiliser AtomicPtr avec des pointeurs de danger pour des mises à jour sans verrou, mais cela nécessite une gestion manuelle complexe de la mémoire et est sujet aux erreurs. Une autre option est d'utiliser un RwLock<Arc<Config>>, permettant des échanges atomiques du pointeur lui-même, mais cela ajoute une indirection et un verrou supplémentaires pour l'échange de pointeurs.
L'équipe a choisi Arc::make_mut car il optimise pour le cas courant : si aucun autre thread ne détient une référence (le compte fort est 1), le thread administrateur modifie les données sur place sans allocation. Si la configuration est partagée, elle clone de manière transparente. Cela nécessite des sémantiques strictes Acquire/Release pour garantir que lorsque le dernier autre lecteur supprime son Arc (en utilisant Release), le contrôle subséquent du thread administrateur (en utilisant Acquire) voit toutes les écritures précédentes dans la configuration, empêchant les lectures déchirées. Le résultat a été une réduction de 40 % de la latence pour les mises à jour de configuration en cas de faible contention.
Pourquoi l'ordre Relaxed ne peut-il pas être utilisé pour la vérification du compte de références dans Arc::make_mut ?
Les opérations Relaxed ne fournissent aucune garantie de se produire avant. Si make_mut utilisait Relaxed pour vérifier si le compte fort est 1, il pourrait observer le décrément du compte d'un autre thread avant d'observer les écritures de ce thread dans les données internes. Cela permettrait au thread actuel de muter les données pendant qu'un autre thread est encore logiquement en train de les lire, causant une course de données. Acquire garantit que lorsque nous voyons le compte atteindre 1 (synchronisé via le Release dans la suppression de l'autre thread), nous voyons également toutes les écritures précédentes dans les données.
Qu'est-ce qui distingue le comportement de Arc::make_mut de la clonage manuel de l'Arc avec .clone() suivi d'une modification ?
Le clonage manuel crée un nouvel Arc pointant vers la même allocation, incrémentant le compte fort à au moins 2. Vous ne pouvez pas obtenir d'accès mutable aux données internes via ce nouvel Arc car Arc ne fournit qu'un partage immuable. Arc::make_mut est spécial car il vérifie si le compte est 1 ; si c'est le cas, il fournit &mut T à l'allocation existante. Sinon, il clone les données dans une nouvelle allocation avec un compte de 1, garantissant que les données partagées originales restent immuables tout en vous donnant une propriété unique de la nouvelle copie.
Comment les pointeurs faibles (Arc::downgrade) affectent-ils la garantie d'unicité de Arc::make_mut ?
Les pointeurs faibles ne participent pas au compte de références fortes. Arc::make_mut ne vérifie que le compte fort, ignorant les références faibles. Cependant, les pointeurs faibles peuvent être mis à niveau vers des pointeurs forts si l'allocation existe toujours. Si make_mut procède à une mutation sur place (le compte fort est 1), et qu'un autre thread met ensuite à niveau un pointeur faible, cette mise à niveau créera un nouvel Arc pointant vers les mêmes données mutées. Cela est sûr car la mise à niveau se fait après la mutation, et le modèle de mémoire de Rust garantit que le pointeur mis à niveau voit la valeur entièrement modifiée. Le compte faible ne prévient pas la mutation, mais il maintient l'allocation en vie même si toutes les références fortes sont temporairement supprimées.