В многопоточном API нужно передать объект трейта в другой поток. Что меняет добавление bound Send к типу trait object?
Send сообщает Rust, что владение значением типа можно безопасно передать в другой поток. Поэтому Box<dyn Job + Send> может быть перемещён в поток, тогда как Box<dyn Job> без этого bound не считается безопасным для передачи.
Однако Send не означает, что объект можно безопасно использовать одновременно из нескольких потоков: для этого обычно требуется Sync. Кроме того, Send не заменяет 'static: API вроде thread::spawn часто требует, чтобы переданное значение не содержало заимствований с ограниченным временем жизни.
В Rust безопасность многопоточности строится вокруг системы типов и владения, а не вокруг обязательной проверки во время выполнения. Маркерные трейты Send и Sync позволяют выразить свойства типов, необходимые для безопасного обмена между потоками.
Это решает исходную проблему передачи произвольных значений в конкурентный код: компилятор должен проверить, что тип не содержит состояния, которое нельзя безопасно переместить или разделять между потоками.
Trait object скрывает конкретный тип за указателем на данные и таблицей виртуальных методов. Компилятор не может исходить только из набора обычных методов трейта: конкретная реализация может содержать, например, Rc, Cell или другой не потокобезопасный компонент.
Если такой объект передать в другой поток без проверки, можно получить гонку данных или нарушить правила владения. Поэтому dyn Trait не получает Send автоматически только потому, что методы трейта выглядят безвредно.
Запись dyn Job + Send означает, что конкретный тип, скрытый за trait object, должен реализовывать Send. При приведении конкретного значения к этому типу Rust проверяет соответствующий bound. Если тип не является Send, такое приведение не компилируется.
Box передаёт владение объектом в замыкание, а Send делает такое перемещение допустимым для другого потока. PrintJob автоматически реализует Send, поскольку его состояние не содержит неотправляемых компонентов.
Без Send тип Box<dyn Job> не обязан быть отправляемым: сам трейт не гарантирует свойства скрытой реализации. Если же объявить trait Job: Send, bound станет обязательным для всех реализаций и отдельное + Send у объекта часто станет избыточным.
Send относится к передаче владения между потоками. Sync означает, что разделяемая ссылка &T может использоваться из нескольких потоков; практически это важно, например, для Arc<dyn Job + Send + Sync>. Эти свойства независимы: Send не подразумевает Sync.
Отдельно проверяется время жизни. thread::spawn не знает, когда завершится новый поток, поэтому передаваемые ему данные обычно должны быть 'static. Это означает отсутствие ссылок на локальные значения, а не обязательное отсутствие динамической памяти.
Основной компромисс — явное ограничение API. Добавляя Send, библиотека допускает только потокобезопасные реализации, но отсекает типы, которые корректны в однопоточном сценарии. Если объект должен лишь вызываться в текущем потоке, такой bound добавлять не следует.
Очередь задач принимает Box<dyn Job>, а рабочий поток получает их через канал. Вариант без bounds прост, но не позволяет безопасно передать очередь задач в рабочий поток: компилятор не может доказать отправляемость объектов.
Можно сделать трейт Job: Send, что гарантирует отправляемость каждой реализации. Это хороший вариант, если сама концепция задачи по смыслу всегда предназначена для фонового выполнения, но он излишне ограничивает повторное использование трейта в однопоточном коде.
Другой вариант — оставить трейт нейтральным и использовать Box<dyn Job + Send + 'static> только в API рабочего потока. Это сохраняет более широкий базовый трейт и локализует требование там, где действительно нужна передача между потоками.
На практике обычно выбирают второй вариант для общей библиотеки: однопоточные пользователи не получают ненужное ограничение, а многопоточный слой явно фиксирует требования Send и 'static. Если задачи должны разделяться между потоками, к bound добавляют также Sync и выбирают подходящую обёртку вроде Arc.
Достаточно ли Send, чтобы несколько потоков одновременно вызывали методы объекта?
Нет. Send разрешает передать владение значением в другой поток, то есть после передачи прежний владелец больше не использует объект. Для безопасного использования через общую ссылку нужен Sync, а для совместного владения обычно применяют Arc и синхронизацию внутреннего состояния.
Почему объект с Send всё равно может не подойти для thread::spawn?
Потому что Send отвечает только за межпоточечную передачу, а spawn должен гарантировать, что замыкание не переживёт данные, на которые оно ссылается. Поэтому замыкание и захваченные значения обычно требуют 'static; объект может быть Send, но содержать ссылку с коротким временем жизни.
Можно ли добавить Send к trait object, если конкретный тип его не реализует?
Нет. dyn Job + Send не превращает неотправляемый тип в отправляемый и не добавляет синхронизацию. Это требование к скрытой реализации: если внутри объекта есть, например, Rc, приведение к Box<dyn Job + Send> будет отвергнуто компилятором.