Глава 18: Обзор стандартной библиотеки

Раздел 6: Работа с потоками и синхронизацией

Содержание: 1. std::thread: Управление потоками 2. std::sync: Синхронизация между потоками 3. std::cell: Управление мутабельностью Практические советы Пример: Комбинирование модулей Упражнение Заключение

В этом разделе мы разберём инструменты стандартной библиотеки Rust для работы с потоками и синхронизацией: std::thread, std::sync и std::cell. Эти модули позволяют писать параллельный код, безопасно обмениваться данными между потоками и управлять мутабельностью в сложных сценариях. Лекция подойдёт как новичкам, желающим понять основы многопоточности, так и опытным разработчикам, стремящимся освоить тонкости Rust. Мы рассмотрим каждый модуль с примерами, нюансами и практическими советами.


1. std::thread: Управление потоками

Модуль std::thread предоставляет средства для создания и управления потоками выполнения (threads). Потоки позволяют выполнять задачи параллельно, используя многоядерность современных процессоров.

Основные функции:

Пример: запуск простого потока:

use std::thread;
use std::time::Duration;

fn main() {
    // Создаём поток
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("Поток: {}", i);
            thread::sleep(Duration::from_millis(100));
        }
    });
    
    // Главный поток
    for i in 1..5 {
        println!("Главный: {}", i);
        thread::sleep(Duration::from_millis(100));
    }
    
    // Ожидаем завершения потока
    handle.join().unwrap();
}
    

Комментарии:

Заметка: Без join главный поток может завершиться раньше, и дочерний поток будет прерван.


2. std::sync: Синхронизация между потоками

Модуль std::sync предоставляет инструменты для безопасного обмена данными между потоками. В Rust строгая модель владения предотвращает гонки данных, но для общей мутабельности нужны дополнительные механизмы, такие как Mutex, RwLock и Arc.

5.2.1 Mutex: Взаимное исключение

Mutex (Mutual Exclusion) обеспечивает доступ к данным только одному потоку одновременно.

Пример: счётчик в нескольких потоках:

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];
    
    for _ in 0..10 {
        let handle = thread::spawn({
            let counter = counter.lock().unwrap();
            || {
                *counter += 1; // Ошибка: counter уже заблокирован
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Итог: {}", *counter.lock().unwrap());
}
    

Ошибка в примере: Этот код не сработает, так как counter.lock() нельзя использовать вне потока напрямую. Вот исправленная версия с Arc:

5.2.2 Arc: Атомарный счётчик ссылок

Arc (Atomic Reference Counting) позволяет безопасно делить данные между потоками.

Исправленный пример:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Итог: {}", *counter.lock().unwrap()); // 10
}
    

Комментарии:

5.2.3 RwLock: Чтение/запись

RwLock позволяет множественным потокам читать данные одновременно, но только одному записывать.

Пример:

use std::sync::{RwLock, Arc};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(42));
    let mut handles = vec![];
    
    // Потоки для чтения
    for _ in 0..3 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let value = data.read().unwrap();
            println!("Чтение: {}", *value);
        });
        handles.push(handle);
    }
    
    // Поток для записи
    let data_writer = Arc::clone(&data);
    let handle = thread::spawn(move || {
        let mut value = data_writer.write().unwrap();
        *value = 100;
        println!("Запись: {}", *value);
    });
    handles.push(handle);
    
    for handle in handles {
        handle.join().unwrap();
    }
}
    

Комментарии:

Предупреждение: Неправильное использование lock или write может привести к взаимной блокировке (deadlock). Всегда проверяйте логику.


3. std::cell: Управление мутабельностью

Модуль std::cell предоставляет типы для управления мутабельностью в однопоточных сценариях, где стандартные правила владения слишком строгие: Cell и RefCell.

5.3.1 Cell

Cell позволяет изменять значение без мутабельной ссылки, но только через копирование или замену.

Пример:

use std::cell::Cell;

fn main() {
    let value = Cell::new(5);
    println!("Начальное: {}", value.get()); // 5
    value.set(10);
    println!("Новое: {}", value.get()); // 10
}
    

Комментарии:

5.3.2 RefCell

RefCell предоставляет динамическую мутабельность с проверкой правил владения во время выполнения.

Пример:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    {
        let mut borrowed = data.borrow_mut();
        borrowed.push(4);
    }
    println!("Данные: {:?}", data.borrow());
}
    

Комментарии:

Заметка: Cell и RefCell не потокобезопасны. Для потоков используйте Mutex или RwLock.


Практические советы


Пример: Комбинирование модулей

Программа с потоками, счётчиком и мутабельностью:

use std::sync::{Arc, Mutex};
use std::thread;
use std::cell::RefCell;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let data = Arc::new(RefCell::new(vec![]));
    let mut handles = vec![];
    
    for i in 0..5 {
        let counter = Arc::clone(&counter);
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
            data.borrow_mut().push(i);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Счётчик: {}", *counter.lock().unwrap());
    println!("Данные: {:?}", data.borrow());
}
    

Этот пример использует thread для параллелизма, Mutex для счётчика и RefCell для списка.


Упражнение

Напишите программу, которая:

  1. Создаёт 3 потока с помощью std::thread.
  2. Использует Arc и RwLock для общего числа.
  3. Каждый поток увеличивает число на 10 (используя запись).
  4. Главный поток читает значение с помощью read после завершения потоков.
  5. Добавьте Cell для хранения количества выполненных потоков.

Подсказка: Используйте join для синхронизации.


Заключение

Модули std::thread, std::sync и std::cell дают полный контроль над параллелизмом и мутабельностью в Rust. Они обеспечивают безопасность и гибкость, но требуют понимания их ограничений. Освоив их, вы сможете писать надёжные многопоточные программы. Далее мы рассмотрим работу с временем и процессами.

Выполните упражнение или переходите дальше!