Rust所有权机制的设计是这门语言的最大特点,也是其内存安全和多线程并发安全的基石。
上一篇详细介绍了Rust语言的所有权机制和内存安全。
本文接着探讨Rust所有权机制下,如何实现多线程的并发数据读写,以及解释为什么说Rust是天然并发安全的一门语言。
struct Point {
x: i32,
y: i32,
}
struct
从C语言借鉴而来,用来定义一个结构体。
Point结构体中包含x和y两个32位int类型的成员。
通过thread::spawn
传入一个闭包,开启Rust的线程,返回线程的引用。
fn test_thread() {
let h = thread::spawn(|| {
for i in 0..10 {
println!("{}", i);
thread::sleep(Duration::from_millis(1000));
}
});
// 主线程等待h线程执行完毕
h.join();
}
如何实现主线程与新开线程的数据共享呢?
通过move关键字,能够解决所有权的单向转移。
// 通过move关键字将转移所有权到闭包里面
// 注意转移所有权后,p1就失去了所有权。
fn test_thread_move() {
let p1 = Point { x: 25, y: 25 };
let h = thread::spawn(move || {
println!("{} {}", p1.x, p1.y);
});
// p1的所有权已经通过move转移,下面这句编译报错"borrow of moved value: `p1`"
// println!("{} {}", p1.x, p1.y);
h.join();
}
到目前为止,一块内存数据的所有权,在同一时刻实际上只会有一个线程拥有所有权。
那么如何实现,多个线程的共享读、甚至是共享读写呢?
Rc
即Reference Count
,引用计数。Rc
保证引用的内存数据在堆内存上进行分配,其实堆内存数据才适合在多线程间共享访问。反过来思考,在线程私有栈上分配的内存,当然不应该在多线程间共享啦。
引用计数的计数值增加是通过Rc::clone
来完成的。
fn test_Rc() {
//多线程数据共享,数据应该在堆上分配,通过Rc智能指针
let p = Rc::new(Point { x: 25, y: 25 });
// Rc所有权的克隆
let p1 = Rc::clone(&p);
let p2 = Rc::clone(&p);
// p/p1/p2这3个智能指针都指向了同一块堆内存(通过引用计数值来跟踪还有几个引用者)
println!("{} {} {}", p.x, p1.x, p2.x);
}
尽管Rc
引用计数能够克隆出多个独立的引用,但是不好意思,Rc
引用无法转移到新的线程去(Rc类型没有实现Send trait)进行访问,否则编译器直接报错。
`std::rc::Rc<Point>` cannot be sent between threads safely
要真正实现跨线程的访问,需要借助Arc
(Atomic Reference Count
),原子引用计数。
// 通过Arc在多线程之间共享读
fn test_Arc() {
let p = Arc::new(Point { x: 25, y: 25 });
// 克隆出所有权p1,然后转移到新线程进行访问
let p1 = Arc::clone(&p);
let h1 = thread::spawn(move || {
println!("{} {}", p1.x, p1.y);
});
// 同理
let p2 = Arc::clone(&p);
let h2 = thread::spawn(move || {
println!("{} {}", p2.x, p2.y);
});
h1.join();
h2.join();
}
至此,我们了解到了,如何在多个线程中对同一块儿内存数据进行共享读操作。
介绍多线程共享写之前,必须先了解Mutex,因为Rust限制多个线程写同一块内存,线程必须先获得这块内存相应的锁。
fn test_Mutex() {
// Rust Mutex保护一份数据,被保护的数据,任何时刻只有一个线程可以修改
// 猜测内存分布上是,p_mutex指向一个智能指针对象(里面含有一把锁),智能指针才指向具体数据
// 另外要完成修改,必须加mut(mutable)关键字
let mut p_mutex = Mutex::new(Point { x: 0, y: 0 });
// Mutex包装的对象上会持有一把锁,写数据之前必须先获得锁
let mut p = p_mutex.lock().unwrap();
p.x += 1;
println!("{} {}", p.x, p.y);
}
通过前面的介绍,我们知道Arc
能够实现多线程共享读数据,mut Mutex
能够保证对数据的写操作是互斥的。
那么一个自然的想法就是,Arc
包装Mutex
就能实现多线程的共享读和互斥写。
fn test_ArcMutex() {
// 内存分布:栈变量p_arc_mutex-->堆Arc对象(内含引用计数)-->堆Mutex对象(内含锁)-->堆对象Point(x,y)
let mut p_arc_mutex = Arc::new(Mutex::new(Point { x: 0, y: 0 }));
// 可以看到,共享的是不可变的堆Arc对象,这叫做“共享不可变”
let p1 = Arc::clone(&p_arc_mutex);
let h1 = thread::spawn(move || {
let mut p = p1.lock().unwrap();
p.x += 1;
println!("{} {}", p.x, p.y);
});
h1.join();
let p = p_arc_mutex.lock().unwrap();
println!("{} {}", p.x, p.y);
}
Mutex
包装过的。Mutex
智能指针保护的数据,任何时刻只有一个线程可以修改,这叫“可变不共享”。Arc
通过增加引用计数值,在多个线程之间共享,但Arc这个对象本身是不变的,这叫做“共享不可变”。