Rust多线程数据共享
在多线程编程中,在线程之间共享数据,主要有两点需要注意:
1、安全的共享数据
2、安全访问共享的数据
「安全的共享数据」
安全的共享数据,意思是说我们需要知道共享的数据的生命周期,主要体现在该什么时候释放数据。
这意味着我们需要追踪共享数据的使用,了解在什么时候没有代码在使用该数据,此时我们可以释放掉此共享的内存。
在一些带GC的语言中如Java、Go之类的,这并不是问题,也不需要我们考虑,因为这些语言在垃圾回收时会通过可达性分析找到没有引用的内存并释放掉。
而在一些自己掌握内存申请释放的语言中如C语言,这一点就需要我们我们考虑了,「Rust看似是不需要自己管理内存,其实只是通过RAII和所有权机制将内存释放的逻辑自动生成」。一般来说都是通过引用计数的方式追踪内存的使用,在引用计数为0时进行内存释放。
「安全访问共享的数据」
安全的访问共享的数据,即保证同一时间只有一个线程对数据进行访问,以保证数据的准确性。
一般通过锁的方式实现,比如互斥锁、读写锁、自旋锁等等。
我倾向于将其分为两类
-
占用CPU:
即通过CPU空转循环调用compare and swap指令获取锁,一般用于数据操作时间短竞争线程少的场景。
-
不占用CPU:
通过系统提供的线程休眠和唤醒调用,在没有获取锁时进行排队,等待锁释放时被唤醒。一般用于数据操作时间长竞争线程多的场景。
不同的语言有不同的实现或者形式,总的来说只有这两类方式实现安全的访问共享数据。
下面时一个Rust多线程共享数据的例子,从例子来看上面的两点在Rust中如何实现。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(5));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut num = data.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *data.lock().unwrap());
}
这个例子开启了10个线程,每个线程将这个数据进行加一操作,非常简单。
在这个例子中,Rust通过使用Arc、clone、move等方式实现了在线程之间安全的共享数据;通过Mutex实现了在线程之间安全的访问共享数据。
安全的共享数据
Arc
Arc即「原子引用计数(Atomic Reference Counted)」。它可以保证在多线程场景中安全地增加和减少引用计数,确保即使多个线程同时访问数据,也不会导致数据竞态条件。
引用计数很简单,就是一个计数器,这个计数器能在多线程之间安全的使用,原理上是使用CPU提供的compare and swap指令实现比较和交换这一操作的原子性,同Atomic类型的底层原理一样。
在内核中,引用计数很常见,主要是用于判断内核对象是否还被其它代码持有,操作系统以此判断是否能释放该对象内存。
clone
在Rust中,Arc作用也是如此,「通过clone操作增加引用计数,在对象drop的时候减少引用计数」,并且在Drop时引用计数减少到0后,可以直接释放掉该对象。
move
这里的move关键字用于将变量的所有权转移到闭包中。这意味着我们通过**let data = Arc::clone(&data);**定义的data所有权转移到了线程闭包中,在线程结束时,会释放Arc的引用,并在引用计数释放到0时释放掉共享的数据。
安全的访问共享数据
Rust提供了多种方式在线程间安全的访问共享数据,Mutex互斥锁只是其中一种,还有RwLock、Automic等。
总结:
-
多线程数据共享分为安全共享数据和安全的访问共享数据 -
Rust通过Arc安全共享数据,通过Mutex、RwLock、SpinLock等机制实现安全的访问共享数据 -
move用于将变量所有权转移到闭包中
本文使用 markdown.com.cn 排版