потокобезопасность в системе типовthread-safety in the type system
Send и Sync — маркерные трейты без методов, кодирующие потокобезопасность прямо в типах: Send означает "можно безопасно переместить в другой поток", Sync — "можно безопасно разделять по &-ссылке между потоками". Компилятор выводит их автоматически и структурно почти для всех типов.Send and Sync are marker traits with no methods, encoding thread-safety directly in the type system: Send means "safe to move to another thread", Sync means "safe to share via a & reference across threads". The compiler derives them automatically and structurally for almost every type.
Send и Sync — маркерные трейты (трейты вообще без методов, существующие исключительно как проверяемая компилятором метка). Тип Send, если значение этого типа безопасно передать/переместить в другой поток. Тип Sync, если несколько потоков могут безопасно одновременно держать разделяемую ссылку (&T) на него — формально, T является Sync тогда и только тогда, когда &T является Send. Оба трейта автоматически (и в подавляющем большинстве случаев корректно) выводятся компилятором для любого типа, состоящего целиком из Send/Sync частей — вручную impl Send for MyType {} пишут крайне редко; иногда требуется явно ОТКАЗАТЬСЯ через impl !Send for MyType {} для типов, чью потокобезопасность нельзя проверить структурно (сырые указатели, некоторые FFI-хендлы).Send and Sync are marker traits (traits with no methods at all — they exist purely as compiler-checked labels). A type is Send if it's safe to move/transfer ownership of a value of that type to another thread. A type is Sync if it's safe for multiple threads to hold a shared reference (&T) to it simultaneously — formally, T is Sync if and only if &T is Send. Both traits are automatically (and correctly, in the vast majority of cases) derived by the compiler for any type composed entirely of Send/Sync parts — you almost never write impl Send for MyType {} by hand; you occasionally need to explicitly opt OUT via impl !Send for MyType {} for types whose thread-safety can't be verified structurally (raw pointers, certain FFI handles).
Это механизм Rust, распространяющий ту же проверяемую на этапе компиляции философию безопасности, что управляет памятью (ownership/borrowing), на ПОТОКОВУЮ безопасность.This is Rust's mechanism for extending the same compile-time-checked-safety philosophy that governs memory safety (ownership/borrowing) to THREAD safety.
Вместо соглашений "просто будь аккуратен" (статус-кво C/C++, где ничто в системе типов не мешает разделить не потокобезопасный тип между потоками — о баге узнают через data race, отладчик или инцидент в проде, если вообще узнают) или дефолта "всё потенциально разделяется, поэтому блокируем всё защитно" (частый ответ других языков ценой повсеместных лишних блокировок), Rust позволяет почти все типы автоматически и корректно классифицировать как безопасные/небезопасные для передачи/разделения, а компилятор ОТКАЗЫВАЕТСЯ компилировать код, нарушающий это.Instead of "just be careful" conventions (the C/C++ status quo, where nothing in the type system stops you from sharing a non-thread-safe type across threads; you find out about the bug via a data race, a debugger, or a production incident, if at all) or a language-wide "everything might be shared, so lock everything defensively" default (a common alternative-language answer, at the cost of pervasive unnecessary locking overhead), Rust lets almost all types be automatically, correctly classified as safe-or-not-safe-to-share/send, and the compiler REFUSES to compile code that would violate this.
Например, попытка передать Rc<T> (см. smart-pointers.html — счётчик ссылок Rc НЕ атомарен, что делает разделение между потоками небезопасным) в порождённый поток даёт ошибку компиляции, а не рантайм data race.For instance, attempting to send an Rc<T> (see smart-pointers.html — Rc's refcount increments/decrements are NOT atomic, making it unsound to share across threads) into a spawned thread produces a compile error, not a runtime data race.
Большинство обычных типов (структуры из i32, String, Vec<T> где T: Send и т.д.) автоматически Send + Sync без единой аннотации. Rc<T> — конкретный пример, специально НЕ являющийся Send (его неатомарный счётчик ссылок гонялся бы при одновременном клонировании/уничтожении в двух потоках), в отличие от Arc<T> (атомарный счётчик), который является Send + Sync, если его содержимое таково же. Cell<T>/RefCell<T> — Send (безопасно переместить в другой поток целиком — никто больше не держит доступ), но НЕ Sync (небезопасно, если несколько потоков одновременно держат &RefCell<T>, поскольку внутренняя бухгалтерия рантайм-проверки заимствований сама не синхронизирована/не атомарна) — тонкий и показательный пример Send-без-Sync.Most ordinary types (structs of i32, String, Vec<T> where T: Send, etc.) are automatically Send + Sync with no annotation needed. Rc<T> is a specific example that is NOT Send (its non-atomic refcount would race if two threads cloned/dropped it concurrently), contrasted with Arc<T> (atomic refcount), which is Send + Sync when its contents are. Cell<T>/RefCell<T> are Send (safe to move to another thread entirely — no one else has a handle) but NOT Sync (unsafe for multiple threads to hold &RefCell<T> simultaneously, since RefCell's runtime borrow-checking bookkeeping itself isn't synchronized/atomic) — a genuinely subtle and illustrative Send-without-Sync example.
fn requires_send<T: Send>(value: T) { // можно переместить в поток// safe to move into a thread }
fn requires_sync<T: Sync>(value: &T) { // можно безопасно разделять по &T// safe to share via &T }
// Rc<i32> НЕ Send и НЕ Sync — неатомарный счётчик// Rc<i32> is neither Send nor Sync — non-atomic refcount
// Arc<i32> и Send, и Sync — атомарный счётчик// Arc<i32> is both Send and Sync — atomic refcount
Реальная сигнатура thread::spawn требует F: FnOnce() -> T + Send + 'static — попытка переместить замыкание с Rc<T> внутри в новый поток даёт реальную ошибку компиляции, тогда как Arc<T> компилируется без проблем.The actual signature of thread::spawn requires F: FnOnce() -> T + Send + 'static — attempting to move a closure containing an Rc<T> into a new thread produces a real compile error, while using Arc<T> instead compiles fine.
let shared = Rc::new(5);
std::thread::spawn(move || println!("{}", shared)); // ОШИБКА: Rc<i32> не Send// ERROR: Rc<i32> is not Send
let shared = std::sync::Arc::new(5);
std::thread::spawn(move || println!("{}", shared)); // OK: Arc<i32> является Send// OK: Arc<i32> is Send
Функция, требующая T: Sync для параметра, разделяемого между потоками, отклонит именно RefCell<T>, тогда как функция, требующая только T: Send для однократно перемещаемого параметра, примет его.A function requiring T: Sync for a shared-across-threads parameter rejects RefCell<T> specifically, while a function only requiring T: Send for a moved-once-to-another-thread parameter accepts it.
fn needs_sync<T: Sync>(_v: &T) {}
fn needs_send<T: Send>(_v: T) {}
let cell = RefCell::new(0);
needs_send(cell); // OK: RefCell<i32> является Send// OK: RefCell<i32> is Send
// needs_sync(&cell); // ОШИБКА: RefCell<i32> не Sync// needs_sync(&cell); // ERROR: RefCell<i32> is not Sync
Компилятор не может проверить, на что реально указывает сырой указатель и как это синхронизировано, — реальная причина, по которой unsafe-обёртки FFI-хендлов часто нуждаются в ручном, тщательно обоснованном unsafe impl Send for MyFfiHandle {} после того, как автор вручную проверил потокобезопасность, скрытую от компилятора за сырым указателем (см. unsafe-raw-pointers.html).The compiler cannot verify what a raw pointer actually points to or how it's synchronized — a real reason unsafe FFI wrapper types often need a manual, carefully-justified unsafe impl Send for MyFfiHandle {} after the author has manually verified the underlying thread-safety the compiler can't see through the raw pointer (see unsafe-raw-pointers.html).
struct MyFfiHandle(*mut std::ffi::c_void); // сырой указатель -> не Send, не Sync по умолчанию// raw pointer -> not Send, not Sync by default
// SAFETY: библиотека C гарантирует, что хендл не используется одновременно из двух потоков// SAFETY: the C library guarantees the handle is never used from two threads at once
unsafe impl Send for MyFfiHandle {}
Обобщённая функция, требующая T: Send + Sync + 'static для хранения значения в разделяемом кэше между потоками, показывает, как эти границы сочетаются с дженериками (см. generics.html), делая требования потокобезопасности всей конкурентной структуры данных явными и проверяемыми компилятором в точке использования.A generic function requiring T: Send + Sync + 'static for storing a value in a shared, cross-thread cache shows how these bounds compose with generics (see generics.html) to make an entire concurrent data structure's thread-safety requirements explicit and compiler-checked at the point of use.
fn make_shared_cache<K, V>() -> Arc<Mutex<HashMap<K, V>>>
where
K: Send + Sync + 'static,
V: Send + Sync + 'static,
{
Arc::new(Mutex::new(HashMap::new()))
}
Send/Sync почти всегда выводятся структурно (если каждое поле Send/Sync, содержащий тип тоже таков) — но это значит, что ОДНО не-Send/не-Sync поле где-то внутри с виду безобидной структуры (например, Rc, вложенный глубоко внутрь для кэширования) делает не-Send/не-Sync ВЕСЬ содержащий тип, иногда порождая запутанную ошибку компиляции далеко от истинной причины, в коде, пытающемся передать/разделить внешнюю структуру между потоками.Send/Sync are almost always auto-derived structurally (if every field is Send/Sync, the containing type is too) — but this means a single non-Send/non-Sync field anywhere inside an otherwise-innocuous-looking struct (e.g. embedding an Rc deep inside for caching purposes) makes the ENTIRE containing struct non-Send/non-Sync too, sometimes producing a confusing compile error far from the actual root cause, in code that tries to send/share the outer struct across threads.
Ручная реализация unsafe impl Send for X {} (включение типа в Send/Sync, когда компилятор не смог проверить это автоматически, обычно из-за сырого указателя или FFI-типа внутри) — настоящее, критичное для безопасности утверждение программиста, что тип действительно безопасно передавать/разделять — ошибиться здесь означает реально внести data race, который система типов больше не поймает, потому что вы явно попросили компилятор перестать проверять.Manually implementing unsafe impl Send for X {} (opting a type INTO Send/Sync when the compiler couldn't verify it automatically, typically because of a raw pointer or FFI type inside) is a genuine safety-critical assertion by the programmer that they have manually verified the type really is safe to send/share — getting this wrong is a real way to introduce an actual data race that the type system will no longer catch, since you've explicitly told the compiler to stop checking.
Send/Sync проверяют потокобезопасность относительно СОБСТВЕННЫХ правил алиасинга/изменения Rust, но не проверяют более широкую логическую корректность паттернов конкурентного доступа — например, два потока, корректно синхронизированных на уровне типов через Arc<Mutex<T>>, всё ещё могут иметь логическую гонку или дедлок на более высоком уровне (несогласованный порядок захвата двух разных мьютексов). Send/Sync ловят важный, но не исчерпывающий класс багов конкурентности, а не "ваша конкурентная программа корректна" в целом.Send/Sync verify thread-safety with respect to Rust's OWN aliasing/mutation rules, but do NOT verify broader logical/business correctness of concurrent access patterns — e.g., two threads correctly synchronized at the type level via Arc<Mutex<T>> can still have a logic-level race condition or deadlock at a higher level, like inconsistent lock-ordering across two different mutexes. Send/Sync catch a specific, important, but not exhaustive class of concurrency bugs, not "your concurrent program is correct" in general.