Потоки с областью действия
Заимствование между потоками
«Потоки с областью действия» — бесплатный урок Learn Rust Coding на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Learn Rust Coding, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Learn Rust Coding содержит 4 уроков всего.
Проблема заимствования в потоках
Замыкание обычного thread::spawn должно иметь время жизни 'static: оно не может заимствовать локальные переменные, поскольку поток может пережить функцию, которой они принадлежат. Поэтому Вы так часто видите move и Arc.
Потоки с ограниченной областью действия решают эту проблему, гарантируя завершение каждого потока до окончания области действия, поэтому заимствование локальных данных становится безопасным.
Почему spawn требует 'static
При использовании thread::spawn порождённый поток может продолжать работу после возврата из main или любой другой функции. Если бы он заимствовал локальную переменную, эта переменная могла бы быть уничтожена, пока поток ещё использует её. Rust запрещает это на этапе компиляции и вынуждает перемещать принадлежащие данные в замыкание.
use std::thread;
fn main() {
let nums = vec![1, 2, 3];
// move transfers ownership into the thread
let handle = thread::spawn(move || {
println!("in thread: {:?}", nums);
});
handle.join().unwrap();
}Знакомство с thread::scope
Стабилизированный в Rust 1.63, std::thread::scope создаёт область действия, в которой потоки могут заимствовать локальные переменные. Область действия блокируется до завершения всех потоков внутри неё, поэтому заимствованные ссылки не могут стать висячими.
Порождайте потоки с помощью s.spawn(...), используя дескриптор области действия s вместо thread::spawn.
use std::thread;
fn main() {
let data = vec![10, 20, 30];
thread::scope(|s| {
s.spawn(|| {
println!("borrowed: {:?}", data);
});
});
// data is still usable here
println!("after scope: {:?}", data);
}Заимствование без move
Внутри thread::scope можно читать локальные переменные по ссылке без move. Несколько потоков с ограниченной областью действия могут одновременно совместно использовать неизменяемое заимствование одних и тех же данных, как обычные ссылки.
use std::thread;
fn main() {
let message = String::from("shared text");
thread::scope(|s| {
s.spawn(|| println!("thread 1 sees: {}", message));
s.spawn(|| println!("thread 2 sees: {}", message));
});
println!("main still owns: {}", message);
}Разделение работы по срезу
Распространённый шаблон — разделить срез и поручить каждому потоку обработку отдельной части. Потоки с ограниченной областью действия упрощают это: каждый поток может напрямую заимствовать часть исходного среза, и копирование не требуется.
use std::thread;
fn main() {
let numbers = [1, 2, 3, 4, 5, 6];
let (left, right) = numbers.split_at(3);
thread::scope(|s| {
s.spawn(|| {
let sum: i32 = left.iter().sum();
println!("left sum: {}", sum);
});
s.spawn(|| {
let sum: i32 = right.iter().sum();
println!("right sum: {}", sum);
});
});
}Сбор возвращаемых значений
Как и обычные потоки, s.spawn возвращает ScopedJoinHandle. Вызовите .join(), чтобы получить возвращаемое потоком значение. Можно собрать дескрипторы, а затем присоединить их после порождения потоков, чтобы получить результаты.
use std::thread;
fn main() {
let inputs = [2, 4, 6];
let mut handles = vec![];
thread::scope(|s| {
for &x in &inputs {
handles.push(s.spawn(move || x * x));
}
let results: Vec<i32> = handles.into_iter()
.map(|h| h.join().unwrap())
.collect();
println!("{:?}", results);
});
}С изменяемыми заимствованиями нужна осторожность
Два потока с ограниченной областью действия не могут одновременно хранить изменяемые заимствования одних и тех же данных: это нарушило бы правила Rust для псевдонимов. Чтобы изменять совместные данные из нескольких потоков, по-прежнему нужен Mutex, но один поток может получить уникальное изменяемое заимствование непересекающихся частей.
Ниже каждый поток изменяет отдельную половину массива с помощью split_at_mut.
use std::thread;
fn main() {
let mut data = [1, 2, 3, 4];
let (a, b) = data.split_at_mut(2);
thread::scope(|s| {
s.spawn(|| { for x in a.iter_mut() { *x *= 10; } });
s.spawn(|| { for x in b.iter_mut() { *x += 100; } });
});
println!("{:?}", data);
}Область действия автоматически присоединяет потоки
Не нужно вызывать join для каждого потока с ограниченной областью действия. Когда замыкание scope возвращает управление, Rust автоматически присоединяет все ещё не присоединённые потоки, прежде чем продолжить выполнение. Поэтому заимствования гарантированно остаются действительными на протяжении всего времени жизни потока.
use std::thread;
use std::time::Duration;
fn main() {
let label = String::from("task");
thread::scope(|s| {
s.spawn(|| {
thread::sleep(Duration::from_millis(30));
println!("{} done", label);
});
println!("spawned, scope will wait");
});
println!("all scoped threads finished");
}Объединение области действия с совместным изменением
Если потокам нужно изменять одно и то же значение, объедините потоки с ограниченной областью действия и Mutex. Arc не нужен, поскольку область действия уже позволяет потокам напрямую заимствовать локальный Mutex.
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
thread::scope(|s| {
for _ in 0..5 {
s.spawn(|| {
let mut n = counter.lock().unwrap();
*n += 1;
});
}
});
println!("counter = {}", *counter.lock().unwrap());
}Потоки с областью действия и обычные потоки: когда что использовать
Используйте потоки с ограниченной областью действия, когда работа ограничена по времени и завершается внутри функции, а Вам нужно заимствовать данные из стека без Arc или копирования.
Используйте thread::spawn, когда поток должен пережить текущую функцию или работать на протяжении всего времени жизни программы. Потоки с ограниченной областью действия не могут выйти за её пределы.
Пример параллельного map
Соберём всё вместе: небольшой параллельный map, который преобразует каждый элемент вектора в отдельном потоке, заимствуя входные данные, а затем собирает результаты в исходном порядке.
use std::thread;
fn parallel_double(items: &[i32]) -> Vec<i32> {
let mut handles = Vec::new();
let mut out = Vec::new();
thread::scope(|s| {
for &x in items {
handles.push(s.spawn(move || x * 2));
}
for h in handles {
out.push(h.join().unwrap());
}
});
out
}
fn main() {
let nums = vec![1, 2, 3, 4];
println!("{:?}", parallel_double(&nums));
}Быстрая проверка
Проверьте, насколько хорошо Вы поняли работу потоков с ограниченной областью действия.
Итоги
Вы узнали о потоках с областью действия:
thread::spawnтребует замыканий с временем жизни'static, а потокам с областью действия они не нужны.thread::scopeпозволяет потокам безопасно заимствовать локальные переменные.- Перед возвратом область действия автоматически дожидается завершения всех потоков.
- Для совместного изменения данных по-прежнему нужен
Mutex, но внутри области действияArcне требуется. - Используйте потоки с областью действия для ограниченного параллелизма внутри одной функции.
Часто задаваемые вопросы
Урок «Потоки с областью действия» бесплатный?
Да — полный текст урока «Потоки с областью действия» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Learn Rust Coding, подпишись на CoddyKit PRO. Курс Learn Rust Coding содержит 4 уроков всего.
Чему я научусь в уроке «Потоки с областью действия»?
Заимствование между потоками Ты практикуешь Learn Rust Coding с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Learn Rust Coding?
Предыдущий опыт не требуется. Learn Rust Coding на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Потоки с областью действия»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Learn Rust Coding?
Да. Каждый урок Learn Rust Coding включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Каналы mpsc
- Общее состояние с Arc/Mutex
- Потоки с областью действия
- Каналы Crossbeam