28_rust_无畏并发
无畏并发
Concurrent:程序不同部分之间独立执行;
Parallel:程序不同部分同时运行。
rust无畏并发:允许编写没有细微Bug的代码。并在不引入新Bug的情况下易于重构。这里所说的“并发”泛指concurrent和parallel。
使用线程同时运行代码
1:1模型:实现线程的方式:通过调用OS的API创建线程。只需较小的运行时。
M:N模型:语言自己实现的线程(绿色线程),需要更大的运行时。
rust:需要权衡运行时的支持,希望尽可能小的运行时支持。rust标准库仅提供1:1模型的线程。
通过spawn创建新线程
通过thread::spawn函数可创建新线程,参数是一个闭包(即在新线程里运行的代码)。
thread::sleep可让当前线程暂停执行。
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("spawn thread {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("main thread {}", i);
thread::sleep(Duration::from_millis(1));// sleep 1ms
}
}
/* 输出结果
main thread 1
spawn thread 1
main thread 2
spawn thread 2
spawn thread 3
main thread 3
main thread 4
spawn thread 4
*/
可看出,主线程结束时,子线程也结束。
通过join Handle来等待所有线程完成
thread::spawn函数返回值类型时JoinHandle,JoinHandle持有值的所有权。调用join方法,可等待对应其它线程完成。
- join方法:调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结。
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("spawn thread {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("main thread {}", i);
thread::sleep(Duration::from_millis(1));// sleep 1ms
}
handle.join().unwrap(); // 会阻止主线程执行,直到子线程结束
}
/*输出
main thread 1
spawn thread 1
spawn thread 2
main thread 2
spawn thread 3
spawn thread 4
main thread 3
main thread 4
spawn thread 5
spawn thread 6
spawn thread 7
spawn thread 8
spawn thread 9
*/
如果将handle.join().unwrap()提前到主函数的打印之前,可看到主函数的打印会在子线程执行完之后,join会阻止主线程执行。
使用move闭包
move闭包通常和thread::spawn函数一起使用,允许使用其它线程的数据,创建线程时,把值的所有权从一个线程转移到另一个线程。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// let hd = thread::std::thread::spawn(|| {
// println!("v={:?}", v); // 这里使用v若没获得所有权,则报错,因为闭包的生命周期比v更长
// });
let hd = thread::std::thread::spawn(move || {
println!("v={:?}", v); // 通过move获得生命周期
});
// drop(v); // 如果使用drop手动释放v,则报错,因为v的所有权已转入闭包
hd.join().unwrap();
}
使用消息传递跨线程传递数据
消息传递:线程(或Actor)通过彼此发送消息(数据)进行通信。当前很流行的技术,且能保证安全并发。
rust: Channel(标准库提供)
Channel
- Channel包含发送端和接收端
- 调用发送端方法发送数据
- 接收端会检查和接收收到的数据
- 若发送端或接收端任意一端被丢弃,则Channel“关闭”
创建Channel
使用mpsc::channel函数创建Channel
- npsc表示multiple producer, single consumer(多个生产者,一个消费者)
- 返回一个tuple(元组),元素分别是发送端、接收端
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel(); //创建channel
let hd = thread::spawn(move || { //必须获得所有权才能发消息
let v = String::from("send in thread");
tx.send(v).unwrap(); // send返回的是Result<T,E>,简单使用unwrap处理如果返回错误则panic
});
let recv = rx.recv().unwrap(); // 同步接收消息,会阻塞在recv函数,返回值是Result<T,E>
println!("{}", recv);
}
发送端send方法
- 参数:待发数据
- 返回值: Result<T, E>,若有问题(如接收端已被丢弃),则返回一错误
接收端方法
- recv()方法:会阻塞当前线程执行,直到channel中有值传入,当有值传入,则返回Result<T,E>,当发送端关闭则收到一错误。
- try_recv()方法:不阻塞线程,调用后立即返回Result<T, E>,若收到数据则返回OK及数据,否则返回错误。通常使用循环调用来检查结果。
channel和所有权转移:所有权在消息传递中非常重要,能够保证编写安全、并发的代码。
发送多个值的例子:
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let hd = thread::spawn(move || {
let vs = vec![String::from("msg1"), String::from("msg2"), String::from("msg3"),];
for v in vs {
tx.send(v).unwrap();
thread::sleep(Duration::from_millis(300));
}
});
for recv in rx { //免去写recv函数
println!("{}", recv);
}
}
通过克隆创建多个发送者
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = mpsc::Sender::clone(&tx); // 克隆一个发送端
thread::spawn(move || {
let vs = vec![String::from("msg1"), String::from("msg2"), String::from("msg3"),];
for v in vs {
tx.send(v).unwrap();
thread::sleep(Duration::from_millis(300));
}
});
thread::spawn(move || {
let vs = vec![String::from("tx1 msg1"), String::from("tx1 msg2"), String::from("tx1 msg3"),];
for v in vs {
tx1.send(v).unwrap();
thread::sleep(Duration::from_millis(300));
}
});
for recv in rx {
println!("{}", recv); // 同时收到两个发送端信息
}
}
/*输出结果
msg1
tx1 msg1
msg2
tx1 msg2
msg3
tx1 msg3
*/
共享状态的并发
rust支持通过共享状态实现并发,channel类似单所有权,一旦将值的所有权转移至channel,就无法再使用。共享内存并发则类似多所有权,多个线程可同时访问同一块内存。
使用Mutex来每次只允许一个线程访问数据。Mutex是mutual exclusion(互斥锁)的简写。在同一时刻,Mutex只允许一个线程访问某些数据。若要访问数据,线程必须首先获取互斥锁(lock),lock数据结构是mutex的一部分,能跟踪数据拥有独占访问权的所有者;mutex通常被描述为通过锁定系统保护其所持有的数据。
Mutex的两条规则:
- 在使用数据前,必须尝试获取锁(lock)
- 使用完mutex所保护的数据,必须对数据进行解锁,以便其它线程可获取锁
Mutex<T>
的API:
- 通过Mutex::new(数据)创建
Mutex<T>
,Mutex<T>
是一个智能指针 - 访问数据前,通过lock方法获取锁,其会阻塞当前线程,lock可能会失败,返回是MutexGuard(智能指针,实现了Deref和Drop)
use std::sync::Mutex;
fn main() {
let m = Mutex::new(3); // 创建一个锁,保护对象是3
{
let mut n = m.lock().unwrap(); // 获取锁,返回值LockResult<MutexGuard<'_, T>>
*n = 5;
} // 走完作用域,自动释放锁
println!("{:?}", m);
}
多线程共享Mutex<T>
use std::sync::Mutex;
use std::thread;
fn main() {
let cnt = Mutex::new(0); // 创建mutex,为计数器,初始值为0
let mut hds = vec![]; // 创建vector,存放handle
for _ in 0..10 { // 创建10个线程,将handle存入vector
let hd = thread::spawn(move || { // 每个线程的目的是获取互斥锁,然后修改一次锁的值,且希望在离开作用域时将锁自动释放
let mut n = cnt.lock().unwrap(); // 但此时这里创建了多个线程,创建第一个线程时move了所有权,第二次创建时就会报无法获取所有权的错误
*n += 1;
});
hds.push(hd);
}
for hd in hds {
hd.join().unwrap(); // 等待所有线程结束
}
println!("{}", *cnt.lock().unwrap());
}
// 编译报错 borrow of moved value: `cnt`
上面代码的报错,是因创建了多个线程,创建第一个线程时move了所有权,第二次创建时就会报无法获取所有权的错误,这里需要使用多线程的多重所有权。
使用Arc<T>
进行原子引用计数,解决并发场景下多重所有权的问题。
Arc<T>
和Rc<T>
类似,可用于并发场景,其中A是atomic(原子的)的缩写。这两者的API相同,但相比Rc需要牺牲一定的性能作为代价,所以代码设计时不会让所有基础类型设计成原子的,标准库类型也默认不使用Arc<T>
。
use std::sync::{Mutex, Arc}; // 引入Arc
use std::thread;
fn main() {
let cnt = Arc::new(Mutex::new(0)); // 创建mutex,为计数器,初始值为0,使用Arc智能指针
let mut hds = vec![]; // 创建vector,存放handle
for _ in 0..10 { // 创建10个线程,将handle存入vector
let cnt = Arc::clone(&cnt); // 每次让引用计数+1
let hd = thread::spawn(move || { // 每个线程的目的是获取互斥锁,然后修改一次锁的值,且希望在离开作用域时将锁自动释放
let mut n = cnt.lock().unwrap(); // 解决多重所有权问题
*n += 1;
});
hds.push(hd);
}
for hd in hds {
hd.join().unwrap(); // 等待所有线程结束
}
println!("{}", *cnt.lock().unwrap()); // 最后打印结果10
}
Mutex<T>
+ Arc<T>
与 RefCell<T>
+ Rc<T>
Mutex<T>
提供了内部可变性。和Cell家族一样- 使用
RefCell<T>
改变Rc<T>
所指向的内容 - 使用
Mutex<T>
改变Arc<T>
所指向的内容 RefCell<T>
有可能导致内存泄露的问题,Mutex<T>
则有死锁风险
通过Send和Sync Trait扩展并发
rust语言并发特性较少,上面并发特性皆来自标准库,而不是语言本身,rust也可自己实现并发,其两个标签trait:(称之为标签trait是因其未实现任何方法):
- std::marker::Sync和std::marker::Send
std::marker::Send
- 实现Send trait的类型可在线程间转移所有权
- rust中几乎所有类型都实现了Send,但
Rc<T>
没有实现Send,所以它只用于单线程 - 任何完全由Send类型组成的类型也被标记为Send
- 除了原始指针外,几乎所有基础类型都是Send
std::marker::Sync
- Sync允许从多线程访问数据
- 实现Sync的类型可安全被多个线程引用
- 如果T是Sync,那么&T就是Send,引用可被安全的送往另一个线程
- 基础类型都是Sync
- 完全由Sync类型组成的类型也是Sync,另外
Rc<T>
、RefCell<T>
和Cell<T>
家族也不是Sync,Mutex<T>
是Sync的
注:手动实现Send和Sync很难保证安全的