各种语言中在多线程间有多种方式可以共享、传递数据,Rust中最常用的方式就是通过消息传递或者将锁和Arc
联合使用,而对于前者,在编程界还有一个大名鼎鼎的Actor线程模型
为其背书,典型的有 Erlang 语言,还有 Go 语言中很经典的一句话:
Do not communicate by sharing memory; instead, share memory by communicating
而后者需要在多线程编程中,保证同步性,当你需要同时访问一个资源、控制不同线程的执行次序时,都需要使用到同步性。这个会在后面的介绍,先说一下消息如何在多线程传递。
// 多/单发送者,单接受者
// 标准库提供了通道std::sync::mpsc,其中mpsc是multiple producer, single consumer的缩写,代表了该通道支持多个发送者,但是只支持唯一的接收者。
// 当然,支持多个发送者也意味着支持单个发送者,我们先来看看单发送者、单接收者的简单例子
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;
fn mpsc_demo() {
// 创建一个消息通道, 返回一个元组:(发送者,接收者)
let (tx, rx) = mpsc::channel();
// 创建线程,并发送消息
thread::spawn(move || { // 使用move将tx的所有权转移到子线程的闭包
// 发送一个数字1, send方法返回Result<T,E>,通过unwrap进行快速错误处理
tx.send(1).unwrap();
tx.send(2).unwrap(); // 发几个接几个
// 下面代码将报错,因为编译器自动推导出通道传递的值是i32类型,那么Option<i32>类型将产生不匹配错误
// tx.send(Some(1)).unwrap()
});
// 在主线程中接收子线程发送的消息并输出,接收消息的操作rx.recv()会阻塞当前线程,直到读取到值,或者通道被关闭
println!("receive {}", rx.recv().unwrap()); // 1
println!("receive {}", rx.recv().unwrap()); // 2
// println!("receive {:?}", rx.try_recv()); // 不阻塞,由于子线程的创建需要时间,因此println!和try_recv方法会先执行,而此时子线程的消息还未被发出。try_recv会尝试立即读取一次消息,因为消息没有发出,此次读取最终会报错
}
// 传递所有权数据
// - 若值的类型实现了Copy特征,则直接复制一份该值,然后传输过去,例如之前的i32类型
// - 若值没有实现Copy,则它的所有权会被转移给接收端,在发送端继续使用该值将报错
fn pass_owner() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let s = String::from("我,飞走咯!");
tx.send(s).unwrap();
//println!("val is {}", s);
});
let received = rx.recv().unwrap();
// String底层的字符串是存储在堆上,并没有实现Copy特征,当它被发送后,会将所有权从发送端的s转移给接收端的received,之后s将无法被使用
println!("Got: {}", received);
}
// 上面讲的都是异步通道,无论接收者是否正在接收消息,消息发送者在发送消息时都不会阻塞。
// 下面说一下同步通道
// 与异步通道相反,同步通道发送消息是阻塞的,只有在消息被接收后才解除阻塞,例如
fn sync_chan() {
let (tx, rx)= mpsc::sync_channel(0); // 消息缓冲
// 该值可以用来指定同步通道的消息缓存条数,当你设定为N时,发送者就可以无阻塞的往通道中发送N条消息,当消息缓冲队列满了后,新的消息发送将被阻塞
let handle = thread::spawn(move || {
println!("发送之前");
tx.send(1).unwrap();
println!("发送之后");
});
println!("睡眠之前");
thread::sleep(Duration::from_secs(3));
println!("睡眠之后");
println!("receive {}", rx.recv().unwrap());
handle.join().unwrap();
// 睡眠之前
// 发送之前
// ···睡眠3秒
// 睡眠之后
// receive 1
// 发送之后
}
// 关闭通道,所有发送者被drop或者所有接收者被drop后,通道会自动关闭。
// 传输多种数据类型的消息通道
// 一个消息通道只能传输一种类型的数据,如果你想要传输多种类型的数据,可以为每个类型创建一个通道,你也可以使用枚举类型来实现
fn send_enum() {
enum Fruit {
Apple(u8),
Orange(String)
}
let (tx, rx): (Sender<Fruit>, Receiver<Fruit>) = mpsc::channel();
tx.send(Fruit::Orange("sweet".to_string())).unwrap();
tx.send(Fruit::Apple(2)).unwrap();
for _ in 0..2 {
match rx.recv().unwrap() {
Fruit::Apple(count) => println!("received {} apples", count),
Fruit::Orange(flavor) => println!("received {} oranges", flavor),
}
}
}
// 容易掉进去的坑
fn drop_send() {
let (send, recv) = mpsc::channel();
let num_threads = 3;
for i in 0..num_threads { // 起3个线程发送消息
let thread_send = send.clone(); // 由于每个send都进行了clone,导致原有的send没有被所有权转移,并drop
thread::spawn(move || {
thread_send.send(i).unwrap();
println!("thread {:?} finished", i);
});
}
// 在这里drop send...
// 通道关闭的两个条件:发送者全部drop或接收者被drop
drop(send); // 自己没有被move掉
for x in recv {
println!("Got: {}", x);
}
println!("finished iterating");
}
fn main() {
mpsc_demo();
pass_owner();
// for 循环接受和发送
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); // 多发送者
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
tx1.send(String::from("hi from cloned tx")).unwrap();
});
//由于两个子线程谁先创建完成是未知的,因此哪条消息先发送也是未知的,最终主线程的输出顺序也不确定
for received in rx {
println!("Got: {}", received);
}
sync_chan();
send_enum();
}
// 如果你需要 mpmc(多发送者,多接收者)或者需要更高的性能,可以考虑第三方库:
// - crossbeam-channel, 老牌强库,功能较全,性能较强,之前是独立的库,但是后面合并到了crossbeam主仓库中
// - flume, 官方给出的性能数据某些场景要比 crossbeam 更好些
本Rust问道系列文章都是基于学习《Rust圣经》文章而来,在此感谢