Использование move-замыканий с потоками
Мы часто используем ключевое слово move с замыканиями, передаваемыми в thread::spawn, потому что в этом случае замыкание будет владеть значениями, которые оно использует из окружающей среды, тем самым передавая владение этими значениями из одного потока в другой. В разделе "Захват окружающей среды с помощью замыканий" мы обсуждали move в контексте замыканий. Теперь мы сосредоточимся больше на взаимодействии между move и thread::spawn.
Обратите внимание в листинге 16-1, что замыкание, которое мы передаем в thread::spawn, не принимает аргументов: мы не используем никаких данных из главного потока в коде созданного потока. Чтобы использовать данные из главного потока в созданном потоке, замыкание созданного потока должно захватить значения, которые ему нужны. Листинг 16-3 показывает попытку создать вектор в главном потоке и использовать его в созданном потоке. Однако это еще не будет работать, как вы вскоре увидите.
Имя файла: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
Листинг 16-3: Попытка использовать вектор, созданный главным потоком, в другом потоке
Замыкание использует v, поэтому оно будет захватывать v и делать его частью окружения замыкания. Поскольку thread::spawn запускает это замыкание в новом потоке, мы должны быть able to access v внутри этого нового потока. Но когда мы компилируем этот пример, мы получаем следующую ошибку:
error[E0373]: closure may outlive the current function, but it borrows `v`,
which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
Rust интерпретирует, как захватывать v, и потому что println! только нуждается в ссылке на v, замыкание пытается взять заимствовать v. Однако есть проблема: Rust не может сказать, как долго будет работать созданный поток, поэтому он не знает, будет ли ссылка на v всегда действительной.
Листинг 16-4 представляет сценарий, в котором более вероятно, что ссылка на v не будет действительной.
Имя файла: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
drop(v); // ой нет!
handle.join().unwrap();
}
Листинг 16-4: Поток с замыканием, которое пытается захватить ссылку на v из главного потока, который удаляет v
Если Rust позволил бы нам запустить этот код, есть вероятность, что созданный поток сразу будет помещен в фон и не запустится совсем. В созданном потоке есть ссылка на v внутри, но главный поток сразу удаляет v, используя функцию drop, которую мы обсуждали в главе 15. Затем, когда созданный поток начинает выполняться, v уже не действителен, поэтому ссылка на него также становится недействительной. Ой нет!
Чтобы исправить ошибку компиляции в листинге 16-3, мы можем использовать совет из сообщения об ошибке:
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
Добавив ключевое слово move перед замыканием, мы заставляем замыкание владеть значениями, которые оно использует, вместо того, чтобы позволить Rustу推断,что оно должно взять заимствовать значения. Модификация листинга 16-3, показанная в листинге 16-5, будет компилироваться и работать, как мы предполагаем.
Имя файла: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
Листинг 16-5: Использование ключевого слова move, чтобы заставить замыкание владеть значениями, которые оно использует
Мы, возможно, будем склонны попробовать то же самое, чтобы исправить код в листинге 16-4, где главный поток вызывал drop, используя move-замыкание. Однако этот метод исправления не сработает, потому что то, что пытается сделать листинг 16-4, запрещено по другому причине. Если мы добавим move в замыкание, мы переместите v в окружение замыкания, и мы больше не сможем вызвать drop для него в главном потоке. Вместо этого мы получим следующую ошибку компиляции:
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not
implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {:?}", v);
| - variable moved due to use in
closure
...
10 | drop(v); // oh no!
| ^ value used here after move
Правила владения Rust снова спасли нас! Мы получили ошибку из кода в листинге 16-3, потому что Rust был консервативным и только взял заимствовать v для потока, что означает, что главный поток теоретически мог бы сделать ссылку созданного потока недействительной. Передав Rustу указание на то, чтобы передать владение v созданному потоку, мы гарантируем Rustу, что главный поток больше не будет использовать v. Если мы изменим листинг 16-4 так же, мы нарушаем правила владения, когда пытаемся использовать v в главном потоке. Ключевое слово move переопределяет консервативный стандарт Rustа по взятию в заим; оно не позволяет нам нарушать правила владения.
Теперь, когда мы рассмотрели, что такое потоки и методы, предоставляемые API потоков, давайте посмотрим на некоторые ситуации, в которых мы можем использовать потоки.