Представьте, что поток передаёт другому потоку общую ссылку на значение типа T. Какой маркер потокобезопасности обязан реализовывать T?
Тип T должен реализовывать маркерный трейт Sync. Это означает, что общую ссылку &T безопасно передавать и использовать из нескольких потоков одновременно.
Send отвечает за безопасную передачу владения самим значением, а Sync — за безопасную передачу общей ссылки на него. Для обычного запуска потока дополнительно нужно обеспечить допустимый срок жизни ссылки.
Многопоточность в системах часто использует общую память: это быстро и удобно, но без строгих гарантий приводит к гонкам данных, повреждению состояния и неопределённому поведению.
Rust переносит значительную часть проверки таких ошибок на компиляцию. Маркеры Send и Sync позволяют библиотекам потоков формулировать требования к типам в системе типов, не полагаясь только на документацию или дисциплину разработчика.
Если несколько потоков получают общую ссылку на объект, каждый из них может читать его одновременно, а некоторые операции могут изменять внутреннее состояние объекта через механизм внутренней изменяемости. Поэтому одной гарантии корректности ссылки недостаточно: сам тип должен безопасно поддерживать совместное использование.
Передача типа, не реализующего Sync, как общей ссылки создаёт риск несогласованных изменений. Например, Cell<T> и RefCell<T> используют проверяемую во время выполнения или обычную внутреннюю изменяемость, но сами по себе не предназначены для конкурентного доступа из нескольких потоков.
В Rust действует связь: T реализует Sync тогда и только тогда, когда &T можно безопасно передавать между потоками, то есть &T реализует Send. Это формулировка именно о совместном использовании ссылочного доступа, а не о передаче владения объектом.
Если поток получает владение значением T, проверяется Send для T. Если он получает &T, проверяется Sync для T. Эти свойства выводятся компилятором для составных типов: тип обычно является Sync, только если все его компоненты допускают безопасное совместное использование.
Sync не означает, что операции над объектом автоматически становятся атомарными. Тип может быть Sync, потому что его методы используют атомики, блокировки или неизменяемое состояние. Если требуется изменение общего объекта, обычно применяют Mutex<T>, RwLock<T> или атомарные типы.
Для std::thread::spawn ссылка должна также жить достаточно долго, обычно иметь срок жизни 'static, поскольку новый поток может продолжать работу после выхода из вызывающей функции. std::thread::scope позволяет безопасно работать с некоторыми ссылками меньшего срока жизни, потому что гарантирует завершение дочерних потоков до выхода из области.
Arc обеспечивает совместное владение, но не делает произвольный тип потокобезопасным. В примере AtomicUsize реализует Sync, поэтому его общую ссылку можно безопасно использовать из потока; атомарная операция задаёт корректное конкурентное изменение.
Сервис хранит общую конфигурацию, которую читают рабочие потоки. В конфигурации разработчик использовал RefCell для ленивого кэша, после чего попытался разместить её внутри Arc и передать потокам. Компилятор отклонил решение, поскольку RefCell не предоставляет межпоточной гарантии Sync.
Рассматривались три варианта. Клонировать конфигурацию для каждого потока просто и устраняет синхронизацию, но может быть дорого для большого состояния. Заменить RefCell на Mutex безопасно, однако каждый доступ к кэшу получает блокировку и может создать конкуренцию. Использовать атомарные типы или lock-free-структуру быстрее для узкого класса состояний, но сложнее в проектировании.
Выбрали неизменяемую конфигурацию без кэша и отдельный локальный кэш в каждом рабочем потоке. Это устранило общую изменяемую память, сохранило дешёвое чтение и оказалось предпочтительнее глобальной блокировки, потому что кэш можно было без проблем дублировать.
&T?Нет. Send для T означает, что можно передать владение самим значением. Для передачи общей ссылки требуется Sync для T, поскольку именно оно гарантирует безопасность конкурентного ссылочного доступа. Тип может быть Send, но не Sync, например объект, который безопасно перемещать между потоками, но нельзя одновременно использовать через общие ссылки.
Arc<T> сам по себе не делает любой T потокобезопасным?Arc решает задачу подсчёта ссылок и совместного владения, но не защищает внутреннее состояние T. Поэтому передача Arc<T> между потоками требует подходящих ограничений на T, обычно Send и Sync. Если T изменяемый и не использует собственную синхронизацию, одного Arc недостаточно.
Arc<Mutex<T>> может быть потокобезопасным, даже если T сам не реализует Sync?Mutex предоставляет эксклюзивный доступ к T через защитный объект, поэтому для Mutex<T> обычно достаточно, чтобы T реализовывал Send: значение должно быть допустимо передавать между потоками, но одновременных ссылок на него наружу не выдаётся. Arc затем позволяет разделять владение самой блокировкой. Это не устраняет блокировки, взаимные блокировки или логические гонки, а только делает доступ к защищённому состоянию проверяемым на уровне типов.