Arc::make_mut пытается предоставить изменяемый доступ к внутренним данным, сначала проверяя, что Arc держит единственную сильную ссылку на выделение. Он выполняет эту проверку с помощью атомарной операции загрузки с порядком Acquire на количестве сильных ссылок. Если это число равно одному, операция продолжается и возвращает изменяемую ссылку; в противном случае она клонирует внутренние данные и обновляет Arc, чтобы указывать на новую выделенную область памяти.
use std::sync::Arc; let mut data = Arc::new(5); *Arc::make_mut(&mut data) += 1; // Клонирует только если разделяемо
Пара Acquire/Release необходима, потому что когда другой поток освобождает свою Arc, он выполняет декремент с Release на количестве ссылок. Загрузка с Acquire в make_mut гарантирует, что все записи в память, совершенные потоком, который освобождает ссылку до декремента, видны текущему потоку, предотвращая гонки данных на внутренних данных.
Рассмотрим сервис агрегации метрик с высокой пропускной способностью, где обновления конфигурации распространяются через Arc<Config>. Тысячи потоков удерживают ссылки для чтения текущих настроек, однако администраторский поток периодически должен настраивать пороги без перезапуска сервиса.
Наивный подход заключается в том, чтобы обернуть Config в RwLock и блокировать его для каждого чтения или клонировать всю структуру для каждого незначительного обновления, независимо от совместного использования. Первое решение страдает от перемещения кеш-линий и накладных расходов на блокировки, в то время как второе затрачивает память и процессорное время на избыточные аллокации, когда конфигурация на самом деле уникальна.
Альтернативой является использование AtomicPtr с указателями опасности для неблокирующих обновлений, но это требует сложного ручного управления памятью и подвержено ошибкам. Другим вариантом является использование RwLock<Arc<Config>>, позволяя атомарные обмены самого указателя, но это добавляет дополнительную косвенность и блокировку для обмена указателями.
Команда выбрала Arc::make_mut, потому что она оптимизирует общий случай: если никакой другой поток не удерживает ссылку (сильное количество равно 1), администраторский поток модифицирует данные на месте без аллокации. Если конфигурация разделяемая, она прозрачно клонирует. Это требует строгой семантики Acquire/Release, чтобы гарантировать, что когда последний другой читатель освобождает свою Arc (используя Release), последующая проверка администратора (с использованием Acquire) видит все предыдущие записи в конфигурации, предотвращая разорванные чтения. Результатом стало снижение задержки на 40% при обновлении конфигурации при низкой конкуренции.
Почему нельзя использовать Relaxed порядок для проверки количества ссылок в Arc::make_mut?
Операции с Relaxed не предоставляют никаких гарантий happens-before. Если make_mut использовал Relaxed для проверки, что сильное количество равно 1, он мог бы увидеть декремент количества от другого потока перед тем, как увидеть записи этого потока во внутренние данные. Это позволило бы текущему потоку изменять данные, пока другой поток все еще логически читает их, что приведет к гонке данных. Acquire гарантирует, что когда мы видим количество, достигающее 1 (синхронизированное через Release в функции drop другого потока), мы также видим все предыдущие записи в данные.
Что отличает поведение Arc::make_mut от ручного клонирования Arc с помощью .clone(), а затем модификации?
Ручное клонирование создает новый Arc, указывающий на то же выделение, увеличивая сильное количество по крайней мере до 2. Вы не можете получить изменяемый доступ к внутренним данным через этот новый Arc, потому что Arc предоставляет только неизменяемое разделение. Arc::make_mut специальный, потому что он проверяет, равно ли количество 1; если да, он предоставляет &mut T для существующей аллокации. Если нет, он клонирует данные в новую аллокацию с количеством 1, гарантируя, что исходные разделяемые данные остаются неизменяемыми, при этом обеспечивая вам уникальное право собственности на новую копию.
Как слабые указатели (Arc::downgrade) влияют на гарантию уникальности Arc::make_mut?
Слабые указатели не участвуют в учете сильных ссылок. Arc::make_mut проверяет только сильное количество, игнорируя слабые ссылки. Однако слабые указатели могут быть преобразованы в сильные, если выделение еще существует. Если make_mut продолжает с изменением на месте (сильное количество равно 1), а другой поток затем обновляет слабый указатель, это обновление создаст новый Arc, указывающий на те же измененные данные. Это безопасно, потому что обновление происходит после изменения, и модель памяти Rust гарантирует, что обновленный указатель видит полностью измененное значение. Слабое количество не препятствует изменению, но оно сохраняет выделение в живых даже в том случае, если все сильные ссылки временно освобождены.