0Pricing
Learn Rust Coding · Урок

Общее состояние с Arc/Mutex

Безопасные общие данные

«Общее состояние с Arc/Mutex» — бесплатный урок Learn Rust Coding на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Learn Rust Coding, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Learn Rust Coding содержит 4 уроков всего.

Почему совместное состояние сложно использовать

Иногда передачи сообщений недостаточно, и нескольким потокам действительно нужно читать и изменять одни и те же данные. Rust не позволяет совместно использовать изменяемое значение между потоками без защиты, поскольку это может привести к гонке данных.

Для этого используются два инструмента:

  • Arc для совместного владения между потоками.
  • Mutex для безопасного исключительного изменения.

Rc не обеспечивает безопасность потоков

Rc предоставляет совместное владение, но только в одном потоке. Его счётчик ссылок не синхронизирован, поэтому компилятор запрещает передавать его между потоками. Для совместного использования в нескольких потоках нужен Arc (атомарно подсчитываемые ссылки).

Arc работает подобно Rc, но изменяет свой счётчик с помощью атомарных операций, благодаря чему его копии безопасны между потоками.

use std::sync::Arc;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    let clone1 = Arc::clone(&data);
    println!("original: {:?}", data);
    println!("clone:    {:?}", clone1);
    println!("count:    {}", Arc::strong_count(&data));
}

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

Mutex оборачивает данные и гарантирует, что одновременно к ним обращается только один поток. Вызовите .lock(), чтобы получить MutexGuard, который разыменовывается во внутреннее значение. Блокировка автоматически освобождается, когда охранный объект выходит из области действия.

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);
    {
        let mut num = m.lock().unwrap();
        *num += 10;
    } // guard dropped here, lock released
    println!("value = {:?}", m.lock().unwrap());
}

Объединение Arc и Mutex

Чтобы совместно изменять данные в нескольких потоках, оберните их в Arc>:

  • Arc позволяет нескольким потокам владеть дескриптором одних и тех же данных.
  • Mutex позволяет каждому потоку безопасно изменять их по очереди.

Скопируйте Arc для каждого потока до его порождения.

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let c = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut n = c.lock().unwrap();
        *n += 1;
    });
    handle.join().unwrap();
    println!("counter = {}", *counter.lock().unwrap());
}

Общий счётчик в нескольких потоках

Классический пример: десять потоков увеличивают общий счётчик. Каждый поток хранит собственную копию Arc и блокирует Mutex, чтобы прибавить единицу. После присоединения всех потоков итоговое значение равно ровно 10, и гонки данных не возникает.

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 c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = c.lock().unwrap();
            *num += 1;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("Result: {}", *counter.lock().unwrap());
}

Область действия блокировки важна

MutexGuard удерживает блокировку, пока не будет удалён. Если удерживать её во время медленной работы, другие потоки будут заблокированы. Делайте критические секции короткими: заблокируйте, измените, освободите. Оборачивание блокировки в небольшой блок гарантирует, что она будет освобождена до начала дополнительной обработки.

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

fn main() {
    let log = Arc::new(Mutex::new(Vec::new()));
    let mut handles = vec![];
    for i in 0..3 {
        let l = Arc::clone(&log);
        handles.push(thread::spawn(move || {
            {
                let mut v = l.lock().unwrap();
                v.push(i);
            } // released quickly
        }));
    }
    for h in handles { h.join().unwrap(); }
    let mut result = log.lock().unwrap().clone();
    result.sort();
    println!("{:?}", result);
}

Взаимные блокировки: реальный риск

Взаимная блокировка возникает, когда два потока удерживают блокировки, нужные друг другу, и оба ждут бесконечно. Rust предотвращает гонки данных, но не предотвращает взаимные блокировки. Чтобы избежать их, всегда блокируйте несколько мьютексов в одном и том же порядке и не удерживайте блокировки долго.

Также не пытайтесь дважды заблокировать один и тот же Mutex в одном потоке: стандартный Mutex не поддерживает повторный захват.

Отравление при аварийном завершении потока

Если поток аварийно завершается, удерживая блокировку, Mutex становится отравленным. Последующие вызовы .lock() возвращают Err, показывая, что данные могут быть несогласованными. Если Вы считаете, что данные всё ещё пригодны, можно восстановить внутренний охранный объект с помощью into_inner() для ошибки.

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

fn main() {
    let data = Arc::new(Mutex::new(0));
    let d = Arc::clone(&data);
    let _ = thread::spawn(move || {
        let mut g = d.lock().unwrap();
        *g = 7;
        panic!("boom"); // poisons the mutex
    }).join();
    match data.lock() {
        Ok(g) => println!("ok: {}", *g),
        Err(poisoned) => println!("recovered: {}", *poisoned.into_inner()),
    }
}

RwLock для множества читателей

Когда операций чтения намного больше, чем операций записи, RwLock может быть быстрее, чем Mutex. Он позволяет выполнять чтение одновременно из нескольких потоков или иметь одного писателя. Используйте .read() для совместного доступа, а .write() — для исключительного.

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

fn main() {
    let config = Arc::new(RwLock::new(String::from("v1")));
    let reader = Arc::clone(&config);
    let r = thread::spawn(move || {
        let val = reader.read().unwrap();
        println!("read: {}", *val);
    });
    r.join().unwrap();
    {
        let mut w = config.write().unwrap();
        *w = String::from("v2");
    }
    println!("final: {}", *config.read().unwrap());
}

Атомарные типы для простых счётчиков

Для одного целочисленного счётчика полноценный Mutex избыточен. Типы вроде AtomicUsize предоставляют обновления без блокировок с помощью методов, таких как fetch_add. Оберните их в Arc, чтобы совместно использовать между потоками. Выберите Ordering; SeqCst — самый простой безопасный вариант по умолчанию.

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];
    for _ in 0..5 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            c.fetch_add(1, Ordering::SeqCst);
        }));
    }
    for h in handles { h.join().unwrap(); }
    println!("count = {}", counter.load(Ordering::SeqCst));
}

Выбор подходящего инструмента

Краткие рекомендации для совместного состояния:

  • Канал: передача владения в виде конвейера.
  • Arc<Mutex>: совместно изменяемая структура со смешанными операциями чтения и записи.
  • Arc<RwLock>: совместно используемые данные, для которых преобладает чтение.
  • Атомарные типы: отдельные примитивные счётчики или флаги.

Предпочитайте самый простой подход, который подходит для задачи; используйте блокировки только тогда, когда передача сообщений плохо описывает её.

Быстрая проверка

Проверьте, насколько хорошо Вы поняли работу с совместным состоянием.

Итоги

Вы научились безопасно совместно использовать состояние между потоками:

  • Arc обеспечивает безопасное для потоков совместное владение, а Rc — нет.
  • Mutex предоставляет исключительное изменение через охранный объект, который автоматически снимает блокировку.
  • Arc<Mutex<T>> — стандартный шаблон для совместно изменяемых данных.
  • Сохраняйте области действия блокировок короткими; помните о взаимных блокировках и отравлении.
  • RwLock подходит для данных, которые в основном читаются, а атомарные типы — для простых счётчиков.

Часто задаваемые вопросы

Урок «Общее состояние с Arc/Mutex» бесплатный?

Да — полный текст урока «Общее состояние с Arc/Mutex» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Learn Rust Coding, подпишись на CoddyKit PRO. Курс Learn Rust Coding содержит 4 уроков всего.

Чему я научусь в уроке «Общее состояние с Arc/Mutex»?

Безопасные общие данные Ты практикуешь Learn Rust Coding с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Learn Rust Coding?

Предыдущий опыт не требуется. Learn Rust Coding на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Общее состояние с Arc/Mutex»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Learn Rust Coding?

Да. Каждый урок Learn Rust Coding включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Каналы mpsc
  2. Общее состояние с Arc/Mutex
  3. Потоки с областью действия
  4. Каналы Crossbeam
← Назад к Learn Rust Coding