Сравните владение через Rc<T> с обычным владением: как определяется момент освобождения общего значения?
При обычном владении значение освобождается, когда его единственный владелец выходит из области видимости или явно уничтожается. В Rc<T> значение освобождается, когда уничтожена последняя сильная ссылка на него: каждая копия Rc увеличивает счётчик владельцев, а её уничтожение уменьшает этот счётчик.
Обычная модель ownership делает освобождение памяти предсказуемым без сборщика мусора, но допускает только одного владельца значения. На практике иногда требуется разделить владение между несколькими частями однопоточного приложения, например между узлами графа или объектами кэша.
Rc<T> решает эту задачу подсчётом ссылок. Он добавляет совместное владение, сохраняя детерминированное освобождение памяти, но ценой дополнительного служебного счётчика и ограничений на многопоточность.
Если передать обычное значение нескольким компонентам, Rust не позволит оставить несколько независимых владельцев: перемещение оставит прежнего владельца недоступным. Глубокое копирование решило бы проблему владения, но могло бы быть дорогим или вообще не иметь корректной семантики для ресурса.
Rc<T> позволяет разделить один объект без копирования самого T. Однако неверное понимание его семантики приводит к преждевременному освобождению при хранении обычной ссылки, к утечкам при циклических ссылках или к ошибкам при попытке использовать Rc между потоками.
Rc<T> хранит объект и счётчик сильных ссылок в общей служебной структуре. Операция Rc::clone создаёт ещё один указатель на тот же объект и увеличивает счётчик; она не клонирует значение T.
Когда сильная ссылка уничтожается, счётчик уменьшается. При достижении нуля объект T уничтожается, после чего освобождается и служебная память Rc. Поэтому время освобождения определяется последней сильной ссылкой, а не областью видимости конкретной переменной.
Rc<T> предназначен для одного потока: он не реализует свойства, необходимые для безопасной передачи между потоками. Для межпоточного совместного владения обычно используют Arc<T>, где операции со счётчиком рассчитаны на конкурирующий доступ.
Сильные ссылки образуют владение, поэтому цикл из Rc может удерживать объекты навсегда: счётчик никогда не станет нулём. Для обратных или наблюдающих связей применяют Weak<T>, которая не препятствует уничтожению объекта; получить значение через неё можно только после проверки, что объект ещё существует.
Rc<T> не делает содержимое автоматически изменяемым. Для совместной мутации в одном потоке обычно комбинируют Rc с RefCell, но тогда проверка правил заимствования переносится с компилятора на время выполнения и при нарушении приводит к панике.
В однопоточном редакторе документов несколько узлов дерева должны ссылаться на общий объект конфигурации, который нельзя эффективно копировать. Рассматривались три варианта: передавать конфигурацию по обычному владению, глубоко клонировать её для каждого узла или использовать Rc<Config>.
Обычное владение не подходит, потому что объект требуется нескольким узлам одновременно. Глубокое клонирование устраняет конфликт владельцев, но увеличивает память и может нарушить требование единого состояния конфигурации.
Выбран Rc<Config>: узлы владеют одним экземпляром, а конфигурация уничтожается после удаления последнего узла. Если бы связи образовывали цикл, обратные ссылки сделали бы через Weak; если бы узлы обрабатывались в разных потоках, вместо Rc выбрали бы Arc с подходящей синхронизацией.
Вопрос: Почему Rc::clone обычно дешевле глубокого клонирования значения?
Ответ: Rc::clone копирует только указатель на общую служебную структуру и увеличивает счётчик сильных ссылок. Само значение T остаётся единственным экземпляром, поэтому стоимость операции не зависит от размера T, если не учитывать стоимость самого атомарно незащищённого изменения счётчика и проверок переполнения.
Это не означает, что Rc::clone всегда бесплатен: у него есть накладные расходы на хранение счётчика и управление его значением. Но он не вызывает Clone::clone для T, поэтому совместное владение и копирование данных — разные операции.
Вопрос: Почему Rc<T> нельзя безопасно передавать между потоками?
Ответ: Счётчик ссылок в Rc не использует атомарные операции. Одновременное увеличение или уменьшение счётчика из разных потоков могло бы привести к гонке данных и некорректному решению о времени уничтожения объекта.
Для совместного владения между потоками используют Arc<T>, чей счётчик рассчитан на конкурентный доступ. Однако Arc<T> сам по себе не делает T потокобезопасным: для изменяемого общего состояния всё равно требуются подходящие средства синхронизации, например Mutex или RwLock.
Вопрос: Каким образом цикл сильных ссылок приводит к утечке памяти и почему Weak<T> его разрывает?
Ответ: Представим два объекта, каждый из которых содержит сильную ссылку на другой. После удаления внешних Rc у каждого объекта всё ещё остаётся по одной сильной ссылке, поэтому ни один счётчик не равен нулю и автоматическое уничтожение не начинается.
Weak<T> учитывается отдельно от сильных ссылок и не поддерживает объект живым. После исчезновения всех сильных ссылок объект уничтожается, а попытка получить его через Weak::upgrade возвращает отсутствие значения. Поэтому Weak подходит для обратных связей, наблюдателей и ссылок на родителя, которые не должны определять время жизни объекта.