50-Arc

Arc — Easy Rust 中文译本

译文 · 基于 Easy Rust

原文链接: https://dhghomon.github.io/easy_rust/Chapter_49.html

中文参考:kumakichi/easy_rust_chs

Arc

你还记得我们用Rc来给一个变量一个以上的所有者。如果我们在线程中做同样的事情,我们需要一个 Arc。Arc的意思是 “atomic reference counter”(原子引用计数器)。原子的意思是它使用计算机的处理器,所以每次只写一次数据。这一点很重要,因为如果两个线程同时写入数据,你会得到错误的结果。例如,想象一下,如果你能在Rust中做到这一点。

1
2
3
4
5
6
7
8
9
// 🚧
let mut x = 10;

for i in 0..10 { // 线程 1
    x += 1
}
for i in 0..10 { // 线程 2
    x += 1
}

如果线程1和线程2一起启动,也许就会出现这种情况。

  • 线程1看到10,写下11,然后线程2看到11,写下12 然后线程2看到11,写入12。到目前为止没有问题。
  • 线程1看到12。同时,线程2看到12。线程一看到13,写下13 线程2也写了13 现在我们有13个,但应该是14个 Now we have 13, but it should be 14. 这是个大问题。

Arc使用处理器来确保这种情况不会发生,所以当你有线程时必须使用这种方法。不过不建议单线程上用Arc,因为Rc更快一些。

不过你不能只用一个Arc来改变数据。所以你用一个Mutex把数据包起来,然后用一个Arc把Mutex包起来。

所以我们用一个Mutex在一个Arc里面来改变一个数字的值。首先我们设置一个线程。

1
2
3
4
5
6
7
8
9
fn main() {

    let handle = std::thread::spawn(|| {
        println!("The thread is working!") // 只是测试一下线程
    });

    handle.join().unwrap(); // 让主线程在此等待,直到子线程结束
    println!("Exiting the program");
}

到目前为止,这个只打印:

1
2
The thread is working!
Exiting the program

很好,现在让我们把它放在for的循环中,进行0..5。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {

    let handle = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("The thread is working!")
        }
    });

    handle.join().unwrap();
    println!("Exiting the program");
}

这也是可行的。我们得到以下结果:

1
2
3
4
5
6
The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program

现在我们再加一个线程。每个线程都会做同样的事情。你可以看到,这些线程是在同一时间工作的。有时会先打印Thread 1 is working!,但其他时候Thread 2 is working!先打印。这就是所谓的并发,也就是 “一起运行"的意思。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn main() {

    let thread1 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 1 is working!")
        }
    });

    let thread2 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 2 is working!")
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Exiting the program");
}

这将打印:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program

现在我们要改变my_number的数值。现在它是一个i32。我们将把它改为 Arc<Mutex<i32>>:一个可以改变的 i32,由 Arc 保护。

1
2
// 🚧
let my_number = Arc::new(Mutex::new(0));

现在我们有了这个,我们可以克隆它。每个克隆可以进入不同的线程。我们有两个线程,所以我们将做两个克隆。

1
2
3
4
5
// 🚧
let my_number = Arc::new(Mutex::new(0));

let my_number1 = Arc::clone(&my_number); // 这个克隆会进入线程 1
let my_number2 = Arc::clone(&my_number); // 这个克隆会进入线程 2

现在,我们已经将安全克隆连接到my_number,我们可以将它们move到其他线程中,没有问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));

    let my_number1 = Arc::clone(&my_number);
    let my_number2 = Arc::clone(&my_number);

    let thread1 = std::thread::spawn(move || { // 只有克隆进入线程 1
        for _ in 0..10 {
            *my_number1.lock().unwrap() +=1; // 锁定 Mutex,再改值
        }
    });

    let thread2 = std::thread::spawn(move || { // 只有克隆进入线程 2
        for _ in 0..10 {
            *my_number2.lock().unwrap() += 1;
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Value is: {:?}", my_number);
    println!("Exiting the program");
}

程序打印:

1
2
Value is: Mutex { data: 20 }
Exiting the program

所以这是一个成功的案例。

然后我们可以将两个线程连接在一起,形成一个for循环,并使代码更短。

我们需要保存句柄,这样我们就可以在循环外对每个线程调用.join()。如果我们在循环内这样做,它将等待第一个线程完成后再启动新的线程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));
    let mut handle_vec = vec![]; // JoinHandle 会放进这里

    for _ in 0..2 { // 做两次
        let my_number_clone = Arc::clone(&my_number); // 启动线程前先做好克隆
        let handle = std::thread::spawn(move || { // 把克隆放进去
            for _ in 0..10 {
                *my_number_clone.lock().unwrap() += 1;
            }
        });
        handle_vec.push(handle); // 保存 handle,以便在循环外调用 join
                                 // 如果不推进 vec,它会在这里直接被丢弃
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // 对所有 handle 调用 join
    println!("{:?}", my_number);
}

最后这个打印Mutex { data: 20 }。

这看起来很复杂,但Arc<Mutex<SomeType>>>在Rust中使用的频率很高,所以它变得很自然。另外,你也可以随时写你的代码,让它更干净。这里是同样的代码,多了一条use语句和两个函数。这些函数并没有做任何新的事情,但是它们把一些代码从main()中移出。如果你很难读懂的话,可以尝试重写这样的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use std::sync::{Arc, Mutex};
use std::thread::spawn; // 现在可以直接写 spawn

fn make_arc(number: i32) -> Arc<Mutex<i32>> { // 只是用来在 Arc 里包一个 Mutex 的函数
    Arc::new(Mutex::new(number))
}

fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // 只是方便写成 new_clone 的函数
    Arc::clone(&input)
}

// 现在 main() 更好读了
fn main() {
    let mut handle_vec = vec![]; // 每个 handle 会放进这里
    let my_number = make_arc(0);

    for _ in 0..2 {
        let my_number_clone = new_clone(&my_number);
        let handle = spawn(move || {
            for _ in 0..10 {
                let mut value_inside = my_number_clone.lock().unwrap();
                *value_inside += 1;
            }
        });
        handle_vec.push(handle);    // handle 建好了,放进向量
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // 让每个线程都等待结束

    println!("{:?}", my_number);
}
最后修改 August 21, 2026: 更新 (76fc81a2e)