После запуска потока компилятор отклоняет этот код. Какое ограничение времени жизни нарушено захваченным значением?
use std::thread;
fn main() {
let message = String::from("готово");
let handle = thread::spawn(|| {
println!("{message}");
});
handle.join().unwrap();
}
thread::spawn требует, чтобы замыкание и захваченные им данные имели время жизни 'static и были передаваемыми между потоками (Send). В примере замыкание не владеет message, а пытается захватить локальную переменную по ссылке, поэтому компилятор не может гарантировать, что ссылка останется действительной до завершения нового потока.
Исправление — передать владение явно через move:
Поток операционной системы может продолжать выполнение независимо от функции, которая его создала. Поэтому обычная ссылка на локальную переменную вызывающей функции небезопасна: эта функция может завершиться раньше нового потока.
В Rust это ограничение выражается на уровне типов, а не проверяется предположениями программиста. Требование 'static у thread::spawn предотвращает передачу в независимый поток ссылок, потенциально указывающих на уже уничтоженные данные.
Локальная переменная message принадлежит main и обычно захватывается замыканием по ссылке, если не указать иной способ захвата. После вызова thread::spawn поток может начать выполняться позже, а main может выйти из текущей области видимости.
Даже немедленный вызов join не меняет контракт thread::spawn: проверка корректности должна быть выполнена до запуска потока. API не обязан анализировать конкретный порядок последующих вызовов и выводить из него более короткое время жизни.
Ключевое слово move заставляет замыкание захватить message по значению. Владение строкой переходит в замыкание, затем вместе с ним — в новый поток. У потока больше нет заимствования из стека вызывающего кода.
'static здесь означает не «значение обязано жить до завершения программы», а «значение не содержит ссылок с более коротким временем жизни». Владеющий String удовлетворяет этому требованию, хотя сам объект будет уничтожен вскоре после завершения потока.
У thread::spawn есть два существенных ограничения:
Send, чтобы его можно было передать другому потоку;Send + 'static, поскольку он возвращается через дескриптор JoinHandle.move решает только вопрос владения. Он не делает произвольный тип потокобезопасным: если замыкание захватывает, например, Rc<T>, компилятор отдельно отклонит код из-за отсутствия Send.
Если требуется действительно заимствовать данные из текущего стека, используется std::thread::scope. Scoped-потоки гарантированно завершаются до выхода из области, поэтому их API может разрешить заимствования с ограниченным временем жизни. Цена этого подхода — нельзя позволить scoped-потоку пережить соответствующую область видимости.
Для совместного владения данными между независимыми потоками обычно применяют Arc<T>. Если данные изменяемы, одного Arc недостаточно: нужны, например, Mutex<T> или атомарные типы. Arc обеспечивает совместное владение, но не автоматически обеспечивает безопасное изменение содержимого.
Сервис создаёт рабочие потоки для обработки конфигурации, загруженной в локальную переменную. Первый вариант — передать конфигурацию в каждый поток по значению. Это просто и исключает гонки, но может дорого копировать большие данные или лишает вызывающий код владения.
Второй вариант — обернуть неизменяемую конфигурацию в Arc и клонировать Arc для каждого потока. Такой подход экономит копирование содержимого и сохраняет независимое время жизни, но добавляет атомарные операции подсчёта ссылок.
Третий вариант — использовать thread::scope, если все рабочие потоки должны завершиться внутри одной операции. Он позволяет заимствовать конфигурацию без Arc, но ограничивает архитектуру: нельзя вернуть дескрипторы этих потоков наружу или продолжить их работу после выхода из области.
Для долгоживущего пула потоков выбран Arc<Config> с неизменяемой конфигурацией. Потоки владеют клонами Arc, не зависят от стека создателя, а отсутствие изменяемого общего состояния устраняет необходимость в блокировках. В результате конфигурация хранится один раз, а жизненный цикл потоков не связан с локальной областью вызова.
'static, что поток обязан работать до завершения процесса?Нет. 'static относится к допустимым ссылкам внутри значения, а не к фактической продолжительности его существования. Владеющий объект, например String, может удовлетворять 'static, а затем быть уничтожен сразу после join.
Поток всё равно может завершиться раньше процесса, если его дескриптор присоединён через join или если процесс завершается по другим причинам. Требование 'static лишь гарантирует, что поток не держит ссылку на данные, которые могут исчезнуть из-за выхода вызывающей функции.
move не делает Rc<T> допустимым для thread::spawn?move переносит владение значением, но не меняет его свойства потокобезопасности. Rc<T> использует неатомарный счётчик ссылок, поэтому совместное использование экземпляров между потоками не гарантируется и тип не реализует Send.
Для передачи владения между потоками обычно используют Arc<T>, чей счётчик ссылок рассчитан на межпоточное совместное владение. Однако внутренний T также должен удовлетворять требованиям конкретного способа использования; Arc<RefCell<T>>, например, не превращается автоматически в безопасное изменяемое общее состояние.
thread::scope предпочтительнее move и Arc?thread::scope полезен для короткой параллельной операции, когда потоки должны завершиться до выхода из области и им нужно читать или изменять локальные данные через заимствования. Это может избежать копирования и атомарного счётчика Arc.
Для долгоживущих потоков, фоновых работников или передачи дескрипторов за пределы текущей функции scoped-подход не подходит. В таких случаях данные должны иметь независимое время жизни, а владение обычно передают через move, Arc и подходящий механизм синхронизации.