Практическая ситуация: почему замыканию, передаваемому в новый поток, часто требуется захватить значение по владению, а не по заимствованию?
Поток может продолжить выполнение после завершения функции, создавшей его, поэтому замыкание не должно хранить заимствование данных из её стека. Захват по владению переносит значение внутрь замыкания и делает его независимым от исходной области видимости. В Rust это обычно явно задаётся ключевым словом move.
Rust проектировался так, чтобы предотвращать use-after-free и гонки данных без сборщика мусора. Для этого время жизни заимствований проверяется компилятором, а владение позволяет точно определить, кто отвечает за существование значения.
При передаче работы в другой поток возникает та же проблема, что и при передаче значения в функцию с более долгим временем жизни: нельзя сохранить ссылку на данные, которые могут быть уничтожены раньше потребителя.
Локальная переменная обычно живёт до выхода из функции. Если замыкание сохранит ссылку на такую переменную, а поток начнёт или завершит работу позже, ссылка может указывать на уже освобождённую память.
Поэтому std::thread::spawn требует, чтобы переданное замыкание имело время жизни 'static и было безопасно передаваемо между потоками через признак Send. 'static в данном случае означает, что замыкание не содержит заимствований данных с более коротким временем жизни; это не означает, что данные обязательно живут до завершения программы.
Без move компилятор выбирает способ захвата по тому, как используется значение: заимствование, изменяемое заимствование или перемещение. Для запуска потока заимствование локальной переменной обычно невозможно, потому что поток может пережить функцию-владельца.
move заставляет замыкание захватить используемые значения по владению. Для типа вроде String это означает перемещение самого значения в состояние замыкания, а не копирование его содержимого. После такого перемещения исходная переменная больше не владеет строкой.
Здесь замыкание владеет message, поэтому его данные сохраняются независимо от стека main. Вызов join нужен уже не для корректности заимствования, а для ожидания завершения потока и обработки возможной паники.
Захват по владению не решает автоматически все проблемы многопоточности. Захваченные значения должны удовлетворять требованиям Send, а совместный доступ к изменяемым данным обычно организуют через потокобезопасные примитивы, например Arc вместе с Mutex или RwLock.
Сервис формирует конфигурацию в локальной переменной и передаёт задачу в фоновый поток. Вариант с обычным захватом по ссылке прост, но компилятор отклоняет его: поток может обращаться к ссылке после выхода функции. Увеличить область жизни локальной переменной вручную нельзя, поскольку её стековая область всё равно принадлежит вызвавшему потоку.
Вариант с клонированием конфигурации устраняет проблему времени жизни, но может быть дорогим по памяти и времени. Вариант с Arc позволяет разделить владение без копирования содержимого, однако добавляет атомарный счётчик ссылок и требует синхронизации, если данные изменяются.
Если фоновая задача должна владеть независимой неизменяемой конфигурацией, выбранный вариант — передать её в замыкание через move. Это устраняет недействительные заимствования и не требует общей синхронизации; результатом становится безопасный запуск без лишней копии.
move означает копирование захваченных значений?Нет. Для типов, не реализующих Copy, захват через move обычно перемещает значение в замыкание. Для Copy-типов значение копируется, потому что семантика Copy разрешает неявную побитовую копию вместо фактического перемещения. Само ключевое слово move задаёт способ захвата, но не меняет правила Copy.
'static всё ещё может быть уничтожено до завершения программы?'static для типа или замыкания означает возможность жить не меньше всего требуемого периода, а не обязательное принудительное существование до конца процесса. Владеющее значение, например String, не содержит короткого заимствования и может удовлетворять 'static, но будет уничтожено, когда его последний владелец выйдет из области видимости. Для потока это может произойти после join или при уничтожении дескриптора, если поток уже завершён.
Да, но обычный spawn всё равно требует замыкание с 'static, поэтому ссылка должна быть гарантированно 'static, например ссылка на действительно статические данные. Для произвольных заимствований существуют scoped-потоки, которые гарантируют завершение дочерних потоков до выхода из ограниченной области. Они позволяют безопасно заимствовать локальные данные, потому что область видимости контролирует, что ссылка не переживёт владельца.