сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Многопоточность Rust

Send и Sync в Rust

Send и Sync

Два трейта, на которых держится вся потокобезопасность языка. Спрашивают, чем они отличаются, кто их реализует, почему Rc нельзя в поток и когда пишут unsafe impl.

Стержень: Send - про передачу владения между потоками, Sync - про разделяемую ссылку; оба выводятся компилятором автоматически по составу типа.

// Формулировки: «в чём разница Send и Sync?», «почему Rc не Send?», «когда Arc<T> сам является Send?»

Определения и связь

Send означает, что значение можно переместить в другой поток. Sync означает, что на значение можно дать ссылку нескольким потокам одновременно; формально T: Sync тогда и только тогда, когда &T: Send.

Оба трейта пустые - в них нет методов. Компилятор выводит их автоматически: структура получает Send, если все поля Send, и то же с Sync. Проверяются они в границах API: thread::spawn требует Send от замыкания, Arc требует Send + Sync от содержимого.

// Ключевая мысль: трейты ничего не обеспечивают, они лишь фиксируют свойство, которое даёт устройство типа. Mutex<T> получает Sync даже для не-Sync T именно потому, что внутри есть блокировка.

Send
значение можно переместить в другой поток
Sync
на значение можно дать ссылку из нескольких потоков

Почему Rc не Send

Клонирование Rc это инкремент обычного, неатомарного счётчика. Два потока, сделавшие это одновременно, могут потерять инкремент: счётчик обнулится раньше времени, значение уничтожится, и оставшиеся ссылки станут висячими. Поэтому Rc не Send, и компилятор ловит попытку отправить его в поток ещё на сборке.

Arc делает те же операции атомарно и потому пригоден для многопотока - платя за это на каждом клоне и уничтожении. Сам Arc<T> является Send только когда T: Send + Sync: второму потоку достаётся ссылка на то же значение (нужен Sync), а уничтожить его может любой из потоков (нужен Send).

// Отсюда знаменитая ошибка на Arc<RefCell<T>>: RefCell не Sync, потому что его счётчики заимствований не синхронизированы. Компилятор выдаёт E0277, и это ровно тот случай, когда ошибка спасает от гонки.

атомарный счётчик
инкремент, неделимый с точки зрения других потоков
E0277
тип не удовлетворяет требуемому трейту (например, Sync)

unsafe impl: обещание, а не механизм

Иногда тип оборачивает сырой указатель на внешний ресурс, и автоматический вывод не срабатывает. Тогда пишут unsafe impl Send for MyType {}, но это обещание, а не выключатель: в коде ничего не меняется, компилятор просто перестаёт возражать.

Прежде чем так делать, стоит проверить три вещи: библиотека под указателем потокобезопасна, нет привязки к потоку-создателю, нет скрытого разделяемого состояния. Ошибка здесь даёт гонку данных, которую borrow checker уже не поймает.

// Обратный приём тоже существует: вставить в структуру PhantomData<*const ()>, чтобы отключить автоматический вывод и запретить передачу типа между потоками.

unsafe impl
ручное обещание, что тип безопасен для потоков
авто-трейт
трейт, выводимый по составу полей без участия автора

Как отвечать: «В чём разница между Send и Sync?»

Send - значение можно переместить в другой поток, владение уезжает целиком. Sync - на значение можно дать ссылку сразу нескольким потокам; формально это то же самое, что &T: Send. Оба трейта пустые и выводятся компилятором автоматически по полям, а проверяются в границах API вроде thread::spawn или Arc. Хороший пример разницы: Rc не Send, потому что его счётчик неатомарный и гонка сломала бы подсчёт ссылок. А RefCell - Send, но не Sync: перемещать его между потоками можно, а разделять по ссылке нельзя, потому что счётчики заимствований не синхронизированы.

Пример с RefCell - то, что отличает понимание от заучивания: он показывает, что Send и Sync действительно независимы.

На чём валятся

  • Путают Send и Sync местами.
  • Считают, что Arc делает содержимое потокобезопасным - он синхронизирует только счётчик.
  • Не могут привести тип, который Send, но не Sync, а это RefCell.
  • Ставят unsafe impl Send, чтобы «заглушить компилятор», не проверив инварианты.
  • Думают, что Send и Sync где-то реализованы вручную для обычных типов - их выводит компилятор.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_send_sync1 / 5
    Почему Rc<T> нельзя отправить в другой поток, а Arc<T> — можно?
    A)Rc не реализует Clone, а без клона передавать нечего
    B)Rc хранит данные на стеке создавшего потока
    C)Arc копирует данные для каждого потока, а Rc разделяет их
    D)Счётчик Rc меняется неатомарно: гонка испортит подсчёт
    показать ответ и разбор
    +D)Счётчик Rc меняется неатомарно: гонка испортит подсчёт

    // разбор: Два потока, одновременно клонирующие Rc, могли бы потерять инкремент — счётчик обнулился бы раньше времени, и получилось бы обращение к освобождённой памяти. Поэтому Rc не Send, и компилятор ловит это на этапе сборки. Arc делает те же операции атомарно, платя за это на каждом клоне и drop.

  2. #rs_send_sync2 / 5
    При каких условиях Arc<T> сам является Send?
    A)Когда T: Send — этого достаточно, ведь Arc синхронизирует доступ
    B)Когда T: Send + Sync — иначе доступ был бы небезопасен
    C)Arc — Send по определению: он создан для потоков
    D)Когда T: Clone, чтобы каждый поток получил свою копию
    показать ответ и разбор
    +B)Когда T: Send + Sync — иначе доступ был бы небезопасен

    // разбор: Передавая Arc в поток, вы даёте второму потоку ссылку на то же значение — значит нужен Sync. Send нужен потому, что уничтожить значение может любой из потоков, где счётчик обнулится последним. Отсюда и запрет на Arc<RefCell<T>>: RefCell не Sync, компилятор роняет сборку.

  3. #rs_send_sync3 / 5
    Кто пишет реализации Send и Sync для обычных типов?
    A)Автор типа объявляет их вручную в impl-блоке
    B)Они проставляются макросом derive при объявлении структуры
    C)Компилятор выводит их автоматически по полям типа
    D)Стандартная библиотека через сплошную реализацию для всех типов
    показать ответ и разбор
    +C)Компилятор выводит их автоматически по полям типа

    // разбор: Это авто-трейты: структура получает Send, если все её поля Send, и то же с Sync. Ручная реализация возможна только через unsafe impl — и это обещание компилятору, что автор обеспечил безопасность сам, обычно вокруг сырых указателей. Обратный приём — вставить PhantomData, чтобы автоматический вывод отключить.

  4. #rs_send_sync4 / 5
    Тип оборачивает сырой указатель на внешний ресурс. Что означает unsafe impl Send для него?
    A)Автор берёт ответственность: компилятор это не проверит
    B)Тип станет потокобезопасным автоматически после такой пометки
    C)Компилятор добавит блокировку вокруг доступа к указателю
    D)Указатель будет копироваться при передаче в другой поток
    показать ответ и разбор
    +A)Автор берёт ответственность: компилятор это не проверит

    // разбор: Send и Sync — обещания, а не механизмы: пометка ничего не меняет в коде, она лишь снимает запрет компилятора. Автор обязан доказать себе, что передача в другой поток безопасна — библиотека под указателем потокобезопасна, нет привязки к потоку-создателю, нет скрытого разделяемого состояния. Ошибка здесь даёт гонку, которую borrow checker уже не поймает.

  5. #rs_send_sync5 / 5
    Откуда у типа берутся Send и Sync, если их никто не писал?
    A)Их приносит derive, который добавляется к каждой структуре по умолчанию
    B)Их добавляет стандартная библиотека сплошной реализацией для всех типов сразу
    C)Они появляются после первой отправки значения в другой поток и кэшируются компилятором
    D)Их выводит компилятор: тип получает трейт, когда все его поля им обладают
    показать ответ и разбор
    +D)Их выводит компилятор: тип получает трейт, когда все его поля им обладают

    // разбор: Это авто-трейты: структура получает Send, если все поля Send, и Sync, если все поля Sync. Поэтому достаточно одного Rc или сырого указателя в глубине состава, чтобы тип перестал ходить между потоками. Написать реализацию руками можно только через unsafe impl, и это уже ваше личное обещание компилятору.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.