Программирование RustUnsafe и памятьRust-разработчик системного ПО

Обёртка над raw pointer помечена как Send. Какой контракт безопасности делает передачу такого значения межд...

Обёртка над raw pointer помечена как Send. Какой контракт безопасности делает передачу такого значения между потоками корректной?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

unsafe impl Send корректен только тогда, когда после передачи значения в другой поток сохраняются все инварианты владения, времени жизни и потокобезопасности объекта, на который указывает указатель. Передаваемый экземпляр должен иметь право единолично управлять объектом либо использовать явно синхронизированный протокол доступа; одного факта, что адрес выглядит корректным, недостаточно.

Send означает возможность переместить само значение между потоками, но не разрешает одновременно использовать один объект из нескольких потоков. Для ресурса FFI дополнительно нужно доказать требования внешней библиотеки: некоторые дескрипторы можно закрывать в любом потоке, а некоторые — только в потоке-владельце.

Исторический контекст

Rust стремится проверять безопасность конкурентного доступа на уровне типов. Для этого существуют маркерные трейты Send и Sync: первый описывает перенос значения между потоками, второй — безопасное совместное использование ссылки на значение.

Raw pointer не содержит информации о владении, времени жизни и протоколе синхронизации. Поэтому компилятор не может вывести из одного типа указателя, допустимо ли передавать его в другой поток. unsafe impl Send позволяет автору низкоуровневой обёртки вручную заявить этот контракт там, где автоматическая проверка невозможна.

Постановка проблемы

Предположим, структура хранит указатель на объект в куче или на ресурс C-библиотеки. Если без доказательства пометить её как Send, другой поток может разыменовать уже освобождённую память, закрыть ресурс одновременно с прежним владельцем или нарушить внутренний потоковый протокол библиотеки.

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

Подробное решение

Контракт Send нужно формулировать для всей обёртки, а не только для поля-указателя. Обычно проверяют следующие свойства:

  • объект по адресу остаётся живым до завершения использования в новом потоке;
  • передаваемая обёртка действительно переносит владение либо получает эксклюзивное право доступа;
  • после перемещения прежний поток не использует указатель;
  • операции чтения и записи соответствуют требованиям Send для типа объекта;
  • освобождение ресурса допустимо из нового потока;
  • для FFI соблюдены документированные ограничения внешней библиотеки.

Для владеющего указателя на обычный Rust-объект условие часто выражают ограничением T: Send. Но это не универсальное доказательство: непрозрачный дескриптор C-библиотеки не становится потокобезопасным только потому, что его представляют как *mut T. Для него нужно опираться на контракт библиотеки и, при необходимости, хранить дополнительный механизм синхронизации или идентификатор потока-владельца.

Минимальный пример владеющей обёртки:

struct Owned<T>(*mut T); unsafe impl<T: Send> Send for Owned<T> {} impl<T> Drop for Owned<T> { fn drop(&mut self) { unsafe { drop(Box::from_raw(self.0)); } } } fn main() { let value = Owned(Box::into_raw(Box::new(42_u64))); std::thread::spawn(move || { unsafe { *value.0 += 1; } drop(value); }).join().unwrap(); }

Здесь передаётся единственный владелец выделенного объекта, а освобождение выполняется ровно один раз. Безусловная реализация Send для любого T была бы ошибочной: например, внутреннее состояние T может требовать обращения только из конкретного потока или иметь собственные ограничения на совместный доступ.

Важно отличать Send от Sync. Send разрешает переместить значение-владельца; он не даёт права клонировать указатель и разыменовывать его параллельно. Если обёртка должна быть доступна через &Wrapper из нескольких потоков, отдельно требуется доказать Sync и безопасность всех операций совместного доступа.

Ситуация из практики

Сервис на Rust получает от C-библиотеки дескриптор контекста и хочет передать его в рабочий поток. Рассматривались три варианта. Безусловно реализовать Send проще всего, но это может привести к гонке с потоком, который продолжает использовать контекст. Завернуть дескриптор в Mutex защищает вызовы, однако не помогает, если сама библиотека запрещает перенос контекста между потоками. Ограничить объект одним потоком безопаснее, но снижает параллелизм.

Выбранное решение — изучить контракт библиотеки, передавать контекст только при явно разрешённой миграции и хранить его в владеющей обёртке с контролем единственного владельца. Если библиотека допускает вызовы из разных потоков, обёртка дополнительно сериализует операции; если нет, вместо Send используется канал команд к потоку-владельцу. Это сохраняет корректность даже тогда, когда C-дескриптор сам по себе выглядит как обычный указатель.

Что кандидаты часто упускают

  1. Достаточно ли ограничения T: Send для unsafe impl<T: Send> Send у указательной обёртки?

    Нет, не всегда. Нужно доказать не только свойства T, но и свойства самой обёртки: владение, отсутствие скрытых алиасов, корректность Drop, допустимость освобождения из другого потока и отсутствие дополнительных внешних ограничений. Для FFI-ресурса параметр Rust-типа часто вообще не описывает реальные правила C-библиотеки.

  2. Что меняется, если обёртка содержит указатель на заимствованные данные?

    Передача должна сохранять время жизни данных и правила заимствования. Простая запись *const T не сообщает компилятору, что указатель связан с исходным владельцем, поэтому безопасная абстракция должна либо не допускать такой перенос, либо кодировать зависимость временем жизни и гарантировать, что владелец переживёт использование в другом потоке. Для указателя на стековые данные передача почти всегда приводит к висячему указателю после выхода из области видимости.

  3. Можно ли после передачи Send оставить копию raw pointer в исходном потоке?

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