Объясните, как работает Rc и Arc в Rust, и когда использовать каждый.

В Rust управление памятью построено на строгой системе владения, где каждый ресурс (например, объект в памяти) должен иметь одного владельца. Это позволяет избегать утечек и гонок данных без сборщика мусора. Однако бывают ситуации, когда необходимо разделить владение — то есть дать нескольким частям кода доступ к одному и тому же ресурсу. Для этого в Rust существуют умные указатели Rc<T> и Arc<T>.

Они решают одну задачу — подсчёт ссылок (reference counting), но применяются в разных контекстах:

  • Rc<T> — для однопоточного кода.

  • Arc<T> — для многопоточного кода.

Разберём оба по порядку.

Rc<T> — Reference Counted

Rc<T> — это негибкий счётчик ссылок, предназначенный для использования внутри одного потока. Он позволяет нескольким владельцам иметь доступ к одному и тому же значению в куче, при этом автоматически отслеживает, сколько владельцев осталось. Когда счётчик ссылок достигает нуля, ресурс освобождается.

Пример:

use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("Привет, мир!"));
let b = Rc::clone(&a);
let c = a.clone(); // тоже самое, что Rc::clone
println!("{}, {}, {}", a, b, c);
// Все три переменные указывают на один и тот же String в куче
}

Ключевые свойства Rc<T>:

  • Обёртывает значение в счётчик ссылок.

  • При клонировании (Rc::clone) не создаёт копию объекта, а увеличивает счётчик.

  • Когда последний Rc уходит из области видимости, объект удаляется.

  • Не потокобезопасен. При попытке передать Rc<T> между потоками будет ошибка компиляции: Rc<T> не реализует Send и Sync.

Когда использовать Rc<T>

  • При реализации графов, деревьев с родителями, AST, где одна структура может ссылаться на одну и ту же подструктуру.

  • В однопоточном GUI или CLI-приложении, где требуется совместное владение объектом.

  • Когда нужен ссылочный тип, живущий столько же, сколько живёт логика программы, но без затрат на клонирование данных.

Пример: двусвязный список или дерево, где родитель ссылается на потомков и наоборот.

Arc<T> — Atomically Reference Counted

Arc<T> — это аналог Rc<T>, но потокобезопасный. Внутри него используется атомарный счётчик ссылок, который позволяет нескольким потокам одновременно владеть одним и тем же объектом. Название означает “Atomic Reference Counting”.

Пример:

use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec!\[1, 2, 3\]);
let mut handles = vec!\[\];
for _ in 0..3 {
let shared_data = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("{:?}", shared_data);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
}

Здесь три потока получают доступ к одним и тем же данным. Благодаря Arc<T> программа компилируется и работает корректно.

Особенности Arc<T>:

  • Реализует Send и Sync — можно передавать между потоками.

  • Клонируется так же, как Rc — увеличивается счётчик.

  • Использует атомарные операции, что делает его медленнее, чем Rc в однопоточном контексте.

  • Сам Arc<T> — не даёт возможности изменять внутренние данные без доп. обёрток.

Если необходимо мутабельное разделение, нужно использовать Arc<Mutex, Arc<RwLock и т. д.

Пример с мьютексом:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec!\[\];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
\*num += 1;
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("Результат: {}", \*counter.lock().unwrap());
}

В этом примере Arc обеспечивает совместный доступ, а Mutex — безопасную мутацию между потоками.

Отличие Rc<T> и Arc<T>

Характеристика Rc<T> Arc<T>
Потокобезопасность ❌ Нет ✅ Да
--- --- ---
Атомарные операции ❌ Нет ✅ Да
--- --- ---
Производительность ✅ Быстрее ❌ Медленнее
--- --- ---
Где использовать Однопоточные задачи Многопоточность, параллельные вычисления
--- --- ---

На что стоит обратить внимание

  1. Нельзя получить мутабельный доступ к данным внутри Rc или Arc без дополнительных обёрток (RefCell, Mutex).

  2. Циклические ссылки (Rc → Rc) приводят к утечкам памяти. Для их избежания применяется Weak<T>, чтобы не увеличивать счётчик ссылок.

  3. Arc не делает данные синхронными по содержимому — только владеет безопасно. Если вам нужен потокобезопасный доступ к мутабельным данным, применяйте Arc<Mutex или Arc<RwLock.

Таким образом, Rc<T> и Arc<T> — важные инструменты в арсенале разработчика на Rust, которые дают возможность реализовать сложные структуры владения, сохраняя при этом безопасность и контроль над ресурсами. Выбор между ними зависит от того, выполняется ли код в одном потоке или нескольких, и нужна ли потокобезопасность.