Rust 学习笔记:消息传递
Rust 学习笔记:消息传递
确保安全并发性的一种日益流行的方法是消息传递,其中线程或参与者通过相互发送包含数据的消息进行通信。
为了实现消息发送的并发性,Rust 的标准库提供了通道(channel)的实现。通道是一个通用的编程概念,数据通过通道从一个线程发送到另一个线程。
通道有两部分:发送端(transmitter)和接收端(receiver)。当通道的任意一端被丢弃了,通道就关闭了。
我们使用 mpsc::channel 函数创建一个新通道:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
mpsc 代表多生产者,单一消费者。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的发送端,但只有一个接收端使用这些值。
channel 函数返回一个元组,其第一个元素是发送端,第二个元素是接收端。
将发送端移动到派生线程中,并让它发送一个字符串,以便派生线程与主线程通信:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}
发送器端有一个 send 方法,它接受我们想要发送的值。send 方法返回 Result<T, E> 类型,因此如果接收方已经被丢弃并且没有地方可以发送值,则发送操作将返回一个错误。
我们将在主线程中从接收器端获取值:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
接收端有两个有用的方法:recv 和 try_recv。
recv 函数将阻塞主线程的执行并等待,直到一个值被发送到通道中。一旦发送了一个值,recv 将在 Result<T, E> 中返回它。当发送器关闭时,recv 将返回一个错误,表示没有更多的值将到来。
try_recv 方法不会阻塞,而是立即返回 Result<T, E>:如果有消息可用,则返回 Ok 值,如果这次没有消息,则返回 Err 值。
如果这个线程在等待消息时还有其他工作要做,那么使用 try_recv 是有用的:我们可以编写一个循环,每隔一段时间调用 try_recv,在消息可用时处理消息,否则在再次检查之前执行其他工作一段时间。
为了简单起见,我们在这个例子中使用了recv。除了等待消息,主线程没有其他工作要做,所以阻塞主线程是合适的。
运行程序,输出:
通道和所有权转移
所有权规则在消息发送中起着至关重要的作用,让我们做一个实验来展示通道和所有权是如何一起工作以防止出现问题的:在将 val 值发送到通道之后,我们将尝试在派生线程中使用 val 值。
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
编译出错:
send 函数拥有其参数的所有权,当值被移动时,接收方也拥有它的所有权。
一旦值被发送给另一个线程,该线程可能会在我们再次尝试使用该值之前修改或删除它。由于数据不一致或不存在,其他线程的修改可能会导致错误或意外结果。Rust 不允许我们在发送值之后再次使用它。
发送多个值并看到接收者正在等待
为证明通道两端的两个线程是并发执行的,我们重新编写一个示例,生成的线程现在将发送多条消息,并在每条消息之间暂停一秒钟。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
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));
}
});
for received in rx {
println!("Got: {received}");
}
}
在主线程中,我们不再显式地调用 recv 函数。我们将 rx 视为迭代器。对于接收到的每个值,我们打印它。当通道关闭时,迭代将结束。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
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));
}
});
for received in rx {
println!("Got: {received}");
}
}
程序输出,每行间隔 1 秒:
因为我们在主线程的 for 循环中没有任何暂停或延迟的代码,所以我们可以知道主线程正在等待从派生线程接收值。
通过 clone 发射端创建多个生产者
前面我们提到 mpsc 是多生产者,单一消费者的缩写。让我们通过克隆发送端来创建多个线程,它们都将值发送到同一个接收方。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
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 {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
程序输出:
我们在发送器 tx 上调用 clone。这将给我们一个新的发送器 tx1。将 tx1传递给第一个派生线程,将 tx 传递给第二个派生线程。每个线程向一个接收者发送不同的消息。
注:每次运行结果的顺可能不同,这是并发性有趣又困难的地方。