В коде поток получает собственное владение Rc<Mutex<i32>>, но компилятор всё равно отклоняет запуск. Объясните, какой механизм делает такую передачу недопустимой.
use std::{rc::Rc, sync::Mutex, thread};
fn main() {
let counter = Rc::new(Mutex::new(0));
let worker_counter = Rc::clone(&counter);
thread::spawn(move || {
*worker_counter.lock().unwrap() += 1;
}).join().unwrap();
}
Код отклоняется, потому что Rc<T> не реализует Send: его счётчик ссылок не является атомарным и не предназначен для межпоточной передачи. Наличие Mutex<i32> защищает доступ к самому числу, но не делает внешний Rc потокобезопасным.
Для передачи разделяемого владения между потоками нужен Arc<T>, а не Rc<T>:
В многопоточных системах обычный счётчик ссылок нельзя безопасно изменять одновременно из разных потоков: операции увеличения и уменьшения могут конфликтовать. В результате возможны потеря обновления, преждевременное освобождение или утечка памяти.
Rust разделяет две задачи: Send разрешает передать владение значением в другой поток, а Sync означает, что общую ссылку на значение можно безопасно использовать из разных потоков. Это позволяет проверять межпоточную безопасность на этапе компиляции, без неявного превращения любого разделяемого объекта в потокобезопасный.
thread::spawn должен иметь возможность переместить захваченные значения в новый поток. Поэтому замыкание и всё его состояние должны удовлетворять ограничениям, необходимым для межпоточного запуска, включая Send и обычно 'static.
Rc использует обычный, неатомарный счётчик ссылок. Даже если в конкретном фрагменте фактически существует только один рабочий клон, тип Rc<T> не даёт гарантии, что его копии никогда не будут использоваться конкурентно, поэтому стандартная библиотека не реализует для него Send и Sync.
Mutex<T> сериализует доступ к значению T: только владелец успешно полученного MutexGuard может обращаться к данным. Но mutex не защищает операции самого счётчика ссылок Rc. Это два независимых уровня синхронизации.
Rc::clone не копирует T; он увеличивает счётчик ссылок. Если такие операции могут выполняться в разных потоках, неатомарный счётчик становится источником гонки данных. Поэтому компилятор запрещает перемещение Rc<Mutex<i32>> в поток ещё до анализа фактического сценария использования.
Arc применяет атомарные операции к счётчику ссылок и потому может быть отправлен между потоками при выполнении ограничений для содержащего типа. Для Arc<Mutex<i32>> они выполнены: i32 можно передавать между потоками, а Mutex предоставляет взаимное исключение.
Важно, что Arc не заменяет mutex. Arc<UnsafeCell<T>> или Arc<RefCell<T>> сами по себе не делают изменяемый доступ безопасным. Arc решает проблему межпоточного владения, а Mutex, RwLock, атомарные типы или другой корректный протокол решают проблему конкурентного доступа к данным.
Компромисс Arc<Mutex<T>> — простая модель владения ценой блокировок, возможного ожидания и риска взаимной блокировки при неверном порядке захвата mutex-ов. Для одного счётчика часто эффективнее AtomicI32 или AtomicUsize, если операция укладывается в атомарную модель.
Сервис собирает статистику из нескольких рабочих потоков. Изначальный вариант использовал Arc<Mutex<HashMap<String, usize>>>: он был корректен и удобен, но при частых обновлениях все потоки конкурировали за один mutex.
Рассматривались три варианта. Arc<Mutex<HashMap<...>>> проще всего внедрить, но создаёт единую точку блокировки. Передача событий через канал уменьшает совместное состояние, однако требует обработки очереди и может увеличить задержку. Набор атомарных счётчиков быстрее для фиксированных метрик, но плохо подходит для динамических ключей.
Для небольшого числа заранее известных метрик выбрали атомарные счётчики, а для динамических ключей оставили локальные карты потоков с периодической отправкой агрегированных результатов через канал. Это уменьшило конкуренцию: рабочие потоки почти не блокировались, а объединение данных выполнялось отдельным этапом.
Вопрос: достаточно ли заменить Rc на Arc, если вместо Mutex<i32> используется RefCell<i32>?
Ответ: нет. Arc делает потокобезопасным счётчик владения, но не внутреннюю модель заимствований RefCell. RefCell проверяет правила заимствования во время выполнения и не предоставляет синхронизации между потоками, поэтому Arc<RefCell<i32>> не удовлетворяет нужным ограничениям Send/Sync. Для конкурентного изменяемого доступа нужен, например, Arc<Mutex<i32>> или атомарный тип.
Вопрос: чем отличается требование Send от требования Sync в этом сценарии?
Ответ: Send относится к передаче самого значения: тип T: Send можно переместить в другой поток. Sync означает, что &T можно безопасно передать и использовать из нескольких потоков; формально это связано с тем, что &T является Send.
В примере замыкание перемещает worker_counter, поэтому критично, чтобы используемый тип можно было отправить в новый поток. Если бы потоки совместно хранили ссылки на один объект, компилятор дополнительно проверял бы его Sync.
Вопрос: почему уникальное фактическое использование Rc не позволяет Rust разрешить этот код?
Ответ: свойства Send и Sync задаются для типа в целом, а не выводятся компилятором из текущего количества клонов. Rc предоставляет операции, рассчитанные на однопоточный доступ, и его API не гарантирует сохранение уникальности при передаче значения.
Если бы Rust разрешал отправлять Rc только в случаях, где сейчас виден один владелец, дальнейшее создание или использование клонов потребовало бы сложного динамического контроля и не изменило бы небезопасность самого типа. Поэтому граница проведена проще и надёжнее: Rc остаётся однопоточным, а для межпоточного владения используется отдельный тип Arc.