English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Конкурентное программирование Rust

Безопасная и эффективная обработка параллелизации является одной из целей создания Rust, в первую очередь решая проблему высокой нагрузки на серверах.

Концепция параллелизации (concurrent) заключается в том, что различные части программы выполняются независимо, что легко путать с концепцией параллелизации (parallel), которая подчеркивает "одновременное выполнение".

Параллелизация часто приводит к параллелизму.

Эта глава рассказывает о концепциях и деталях программирования, связанных с параллелизацией.

Поток

Поток (thread) - это независимая часть программы, которая выполняется.

Разница между потоками и процессами заключается в том, что потоки являются концепцией внутри программы, и программы часто выполняются в процессе.

В среде с операционной системой процессы часто выполняются по轮流ому принципу, а потоки управляются внутри процесса программой.

Из-за вероятности возникновения параллелизма в многопоточности, ошибки deadlock и delay, которые могут возникнуть в параллелизации, часто встречаются в программах с механизмом параллелизации.

Чтобы решить эти проблемы, многие другие языки (например, Java, C#) используют специальные инструменты выполнения (runtime), что无疑大大降低了程序的执行效率.

Язык C/C++ также поддерживает многопоточность на уровне самых нижних слоев операционной системы, но сам язык и его компилятор не имеют способности обнаруживать и избегать параллельных ошибок, что создает значительное давление на разработчиков, которые должны тратить много усилий, чтобы избежать ошибок.

Rust не зависит от среды выполнения, что похоже на C/C++.

Но Rust уже в языковой структуре设计了包括所有权机制在内的手段来尽可能地在编译阶段消除最常见的错误, что отсутствует в других языках.

Но это не означает, что мы можем неаккуратно программировать. До сих пор проблемы, связанные с параллельным выполнением, еще не полностью решены в общественном масштабе, и все еще可能出现 ошибки, поэтому при параллельном программировании следует быть особенно внимательным!

В Rust новый процесс создается через функцию std::thread::spawn:

use std::thread;
use std::time::Duration;
fn spawn_function() {
    for i in 0..5 {
        println!("запущенный поток threads print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}
fn main() {
    thread::spawn(spawn_function);
    for i in 0..3 {
        println!("основной поток threads print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

Результат выполнения:

основной поток threads print 0
запущенный поток threads print 0
основной поток threads print 1
запущенный поток threads print 1
основной поток threads print 2
запущенный поток threads print 2

Этот результат может меняться в некоторых случаях, но в общем это так и печатают.

Эта программа имеет подпоток, цель которого - напечатать 5 строк текста, а основной поток напечатает три строки текста, но显然, с завершением основного потока, запущенные потоки также завершаются, и не заканчивается все打印ание.

функция std::thread::spawn принимает параметром функция без параметров, но такое написание не рекомендуется, мы можем использовать closures для передачи функции в качестве параметра:

use std::thread;
use std::time::Duration;
fn main() {
    thread::spawn(|| {
        for i in 0..5 {
            println!("запущенный поток threads print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 0..3 {
        println!("основной поток threads print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

закрытые функции могут сохраняться в переменные или передаваться в другие функции в качестве параметров. Закрытые функции эквивалентны lambda-выражениям в Rust, формат которых следующий:

|параметр1, параметр2, ...| -> тип_возврата {
    // тело функции
}

например:

fn main() {
    let inc = |num: i32| -> i32 {
        num + 1
    };
    println!("inc(5) = {}", inc(5));
}

Результат выполнения:

inc(5) = 6

закрытые функции могут опускать типовое объявление и использовать автоматическую типовую оценку Rust:;

fn main() {
    let inc = |num| {
        num + 1
    };
    println!("inc(5) = {}", inc(5));
}

результат не изменился.

метод join

use std::thread;
use std::time::Duration;
fn main() {
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("запущенный поток threads print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 0..3 {
        println!("основной поток threads print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
    handle.join().unwrap();
}

Результат выполнения:

основной поток threads print 0 
запущенный поток threads print 0 
запущенный поток threads print 1 
основной поток threads print 1 
запущенный поток threads print 2 
основной поток threads print 2 
запущенный поток threads print 3 
запущенный поток threads print 4

Метод join позволяет остановить выполнение программы только после завершения работы подпрограммы.

Принудительное передание собственности

Это часто встречающаяся ситуация:

use std::thread;
fn main() {
    let s = "hello";
    
    let handle = thread::spawn(|| {
        println!("{}", s);
    });
    handle.join().unwrap();
}

Попытка использования ресурсов текущей функции в подпрограмме определённо ошибка! Потому что механизм собственности запрещает возникновение таких опасных ситуаций, которые могут разрушить определённость разрушения ресурсов механизма собственности. Мы можем использовать ключевое слово move в обёртке:

use std::thread;
fn main() {
    let s = "hello";
    
    let handle = thread::spawn(move || {
        println!("{}", s);
    });
    handle.join().unwrap();
}

Передача сообщений

Одним из основных инструментов для реализации передачи сообщений и параллельного выполнения в Rust является канал (channel), который состоит из двух частей: отправителя (transmitter) и получателя (receiver).

Пакет std::sync::mpsc содержит методы для передачи сообщений:

use std::thread;
use std::sync::mpsc;
fn main() {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
    let received = rx.recv().unwrap();
    println!("Получено: {}", received);
}

Результат выполнения:

Получено: hi

Подпрограмма получила отправителя из основного потока tx, и вызвал его метод send для отправки строки, после чего основная программа через соответствующего получателя rx получила её.