Как я могу разделить изменчивый объект между потоками, используя Arc?

Я пытаюсь разделить изменчивый объект между потоками в Rust, используя Arc, но я получаю эту ошибку:

error[E0596]: cannot borrow data in a '&' reference as mutable
  --> src/main.rs:11:13
   |
11 |             shared_stats_clone.add_stats();
   |             ^^^^^^^^^^^^^^^^^^ cannot borrow as mutable

Это пример кода:

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

fn main() {
    let total_stats = Stats::new();
    let shared_stats = Arc::new(total_stats);

    let threads = 5;
    for _ in 0..threads {
        let mut shared_stats_clone = shared_stats.clone();
        thread::spawn(move || {
            shared_stats_clone.add_stats();
        });
    }
}

struct Stats {
    hello: u32,
}

impl Stats {
    pub fn new() -> Stats {
        Stats { hello: 0 }
    }

    pub fn add_stats(&mut self) {
        self.hello += 1;
    }
}

Что я могу сделать?

Ответ 1

Arc документация гласит:

Совместно используемые ссылки в Rust по умолчанию не допускают мутации, и Arc не является исключением: обычно вы не можете получить изменяемую ссылку на что-то внутри Arc. Если вам нужно выполнить мутацию через Arc, используйте Mutex, RwLock или один из типов Atomic.

Скорее всего, вам понадобится Mutex в сочетании с Arc:

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

struct Stats;

impl Stats {
    fn add_stats(&mut self, _other: &Stats) {}
}

fn main() {
    let shared_stats = Arc::new(Mutex::new(Stats));

    let threads = 5;
    for _ in 0..threads {
        let my_stats = shared_stats.clone();
        thread::spawn(move || {
            let mut shared = my_stats.lock().unwrap();
            shared.add_stats(&Stats);
        });
        // Note: Immediately joining, no multithreading happening!
        // THIS WAS A LIE, see below
    }
}

Это в значительной степени взято из документации Mutex.

Как я могу использовать shared_stats после for? (Я говорю об объекте Stats). Похоже, что shared_stats нельзя легко преобразовать в Stats.

Начиная с Rust 1.15, можно вернуть значение. Смотрите мой дополнительный ответ и для другого решения.

[Комментарий в примере] говорит, что многопоточности нет. Почему?

Потому что я запутался! :-)

В примере кода результат thread::spawn (a JoinHandle) немедленно удаляется, поскольку он нигде не сохраняется. Когда ручка уронена, нить отсоединяется и может или не может закончить. Я путал его с JoinGuard, старым удаленным API, который включался при удалении. Извините за путаницу!


Для некоторой редакционной статьи я предлагаю полностью избежать изменчивости:

use std::{ops::Add, thread};

#[derive(Debug)]
struct Stats(u64);

// Implement addition on our type
impl Add for Stats {
    type Output = Stats;
    fn add(self, other: Stats) -> Stats {
        Stats(self.0 + other.0)
    }
}

fn main() {
    let threads = 5;

    // Start threads to do computation
    let threads: Vec<_> = (0..threads).map(|_| thread::spawn(|| Stats(4))).collect();

    // Join all the threads, fail if any of them failed
    let result: Result<Vec<_>, _> = threads.into_iter().map(|t| t.join()).collect();
    let result = result.unwrap();

    // Add up all the results
    let sum = result.into_iter().fold(Stats(0), |i, sum| sum + i);
    println!("{:?}", sum);
}

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