Какой механизм позволяет безопасно использовать Mutex<T> между потоками, даже если T не реализует Sync?
Mutex<T> может сделать доступ к T потокобезопасным, если T реализует Send, даже когда сам T не реализует Sync. Sync требуется для безопасного совместного доступа к значению через общую ссылку, а Mutex не предоставляет такой доступ напрямую: он выдаёт доступ к T только владельцу захваченной блокировки. Однако T всё равно должен быть Send, потому что защищённое значение может перемещаться между потоками вместе с самим Mutex.
В многопоточном коде нужно различать две задачи: передачу владения значением между потоками и одновременный доступ к одному значению. В Rust для них существуют разные маркеры: Send описывает допустимость перемещения владения, а Sync — допустимость использования общей ссылки из нескольких потоков.
Типы вроде Cell<T> допускают изменение через общую ссылку и поэтому обычно не являются Sync. Но если их владение можно безопасно перемещать между потоками, они могут быть Send. Mutex решает проблему совместного доступа внешней синхронизацией, не требуя, чтобы сам T умел безопасно обслуживать параллельные общие ссылки.
Предположим, несколько потоков должны работать с одним значением, которое нельзя напрямую разделять между потоками. Без синхронизации параллельная запись или чтение могут нарушить инварианты значения и привести к гонке данных.
Наивное требование T: Sync было бы слишком строгим: оно исключило бы типы, безопасные при последовательном доступе. Но снятие всех ограничений тоже было бы ошибкой: если T нельзя перемещать между потоками, сама передача защищённого объекта уже небезопасна.
Sync означает, что общая ссылка &T может быть безопасно передана между потоками; формально, тип T является Sync, если &T является Send. Mutex<T> меняет модель доступа: потоки не получают произвольную общую ссылку на содержимое, а сначала захватывают блокировку и получают охраняемый доступ через MutexGuard.
Поэтому для Mutex<T> важен прежде всего T: Send. Защищённое значение должно быть допустимо перемещать между потоками, но параллельный доступ к нему сериализуется мьютексом. В стандартной библиотеке Mutex<T> реализует Send и Sync при условии T: Send.
Arc<Mutex<T>> обычно используют для разделения владения самим мьютексом. Arc обеспечивает потокобезопасное управление счётчиком ссылок, а Mutex — эксклюзивный доступ к содержимому. Arc сам по себе не делает произвольный T потокобезопасным: если T не Send, комбинация Arc<Mutex<T>> не будет допущена к передаче между потоками.
Cell<i32> не является Sync, но является Send, поэтому его можно поместить в Mutex и передать через Arc между потоками. Пока один поток владеет MutexGuard, другой поток не получает доступ к содержимому. Если T не является Send, например из-за привязки к конкретному потоку, Mutex не может легально сделать передачу безопасной.
Это не означает, что мьютекс автоматически решает все проблемы. Нужно удерживать блокировку на всём участке, где поддерживается инвариант, не допускать неправильного порядка захвата нескольких мьютексов и учитывать отравление блокировки после паники. Кроме того, обычный Mutex блокирует поток и потому не должен бездумно использоваться внутри async-задачи однопоточного runtime.
В сервисе несколько рабочих потоков обновляют состояние, представленное типом с изменяемым внутренним содержимым, который не реализует Sync. Рассматривались три варианта: добавить ручную небезопасную реализацию Sync, заменить тип на атомарный или обернуть его в Arc<Mutex<_>>.
Ручная реализация Sync опасна: она возлагает доказательство корректности на разработчика и может привести к неопределённому поведению. Атомарный тип эффективнее, но подходит только для простого состояния с подходящей атомарной моделью и не заменяет защиту составных инвариантов.
Выбран был Arc<Mutex<_>>, поскольку операции над состоянием должны выполняться как единая критическая секция, а тип можно безопасно перемещать между потоками. Решение сохранило проверку гарантий на уровне компилятора; после измерений выяснилось, что конкуренция за блокировку мала, поэтому более сложная схема с отдельным владельцем состояния и каналами не потребовалась.
Достаточно ли T: Send, чтобы несколько потоков одновременно вызывали методы T через Mutex?
Нет, вызовы не выполняются одновременно через один и тот же Mutex: каждый поток сначала захватывает блокировку и получает эксклюзивный MutexGuard. Если нужно только совместное чтение без изменения, можно рассмотреть RwLock, но его внутренние ограничения также не отменяют требований Send и Sync для соответствующих способов использования.
Почему Mutex<T> не может разрешить случай, когда T не реализует Send?
Блокировка сериализует доступ, но не меняет свойства перемещения значения. Сам Mutex<T> может быть создан в одном потоке, а затем передан в другой; значит, защищённое содержимое потенциально покидает исходный поток. Если T привязан к потоку или небезопасен при перемещении, мьютекс не устраняет эту проблему, поэтому реализация Send для Mutex<T> требует T: Send.
Чем Arc<Mutex<T>> отличается от Rc<RefCell<T>> в многопоточном коде?
Rc использует неатомарный счётчик ссылок и не является Send или Sync, поэтому его нельзя разделять между потоками. RefCell проверяет правила заимствования во время выполнения, но не предоставляет межпоточную синхронизацию. Arc<Mutex<T>> использует атомарное управление общим владением и блокировку, однако имеет накладные расходы и может привести к блокировкам или взаимным блокировкам при ошибочной организации доступа.