Объясните, как работает 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
Пример с мьютексом:
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> |
---|---|---|
Потокобезопасность | ❌ Нет | ✅ Да |
--- | --- | --- |
Атомарные операции | ❌ Нет | ✅ Да |
--- | --- | --- |
Производительность | ✅ Быстрее | ❌ Медленнее |
--- | --- | --- |
Где использовать | Однопоточные задачи | Многопоточность, параллельные вычисления |
--- | --- | --- |
На что стоит обратить внимание
-
Нельзя получить мутабельный доступ к данным внутри Rc или Arc без дополнительных обёрток (RefCell, Mutex).
-
Циклические ссылки (Rc → Rc) приводят к утечкам памяти. Для их избежания применяется Weak<T>, чтобы не увеличивать счётчик ссылок.
-
Arc не делает данные синхронными по содержимому — только владеет безопасно. Если вам нужен потокобезопасный доступ к мутабельным данным, применяйте Arc<Mutex
или Arc<RwLock .
Таким образом, Rc<T> и Arc<T> — важные инструменты в арсенале разработчика на Rust, которые дают возможность реализовать сложные структуры владения, сохраняя при этом безопасность и контроль над ресурсами. Выбор между ними зависит от того, выполняется ли код в одном потоке или нескольких, и нужна ли потокобезопасность.