线程
线程的创建与使用
创建线程通常被称为 产生线程 (spawning thread)。我们使用 std::thread::spawn 函数,该函数接收一个 FnOnce 型的闭包, 我们产生的子线程可能会比父线程 活 的更久,这就意味着需要通过 move 来将其所有权转到子线程中
// std::thread::spawn
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
Builder::new().spawn(f).expect("failed to spawn thread")
}
产生线程的时候,这些线程被认为是从父线程复刻(fork)出来的,这也就是我们常说的 fork 一个线程
fn main() {
let duration = Duration::from_millis(3000);
let handle1 = thread::spawn(move || {
thread::sleep(duration);
println!("线程1 执行完成");
});
let handle2 = thread::spawn(move || {
thread::sleep(duration);
println!("线程2 执行完成");
});
// 等待线程结束
handle1.join().unwrap();
handle2.join().unwrap();
println!("程序 执行结束");
}
// 运行结果
// 线程1 执行完成
// 线程2 执行完成
// 程序 执行结束
上面的代码是创建了两个线程,在每个线程里面 休息3秒钟 后输出 线程x 执行完成,最后再输出 程序 执行结束。我们使用 join 将线程捆绑合并在一起。join 函数的作用就是等待另一个线程执行结束
跨线程共享“不可变数据”
fn main() {
let duration = Duration::from_millis(1000);
// 这样可以解决问题,但在数据量很大的时候,不推荐
// let data = vec!["hello".to_string(), "rust".to_string()];
// Arc 原子引用计数
let data: Arc<Vec<String>> = Arc::new(vec!["hello".to_string(), "rust".to_string()]);
// clone 仅会增加引用计数,不会真正的 clone 数据
let handle1_data = data.clone();
let handle1 = thread::spawn(move || {
thread::sleep(duration);
println!("线程1 {:?}", handle1_data);
println!("线程1 执行完成");
});
// clone 仅会增加引用计数,不会真正的 clone 数据
let handle2_data = data.clone();
let handle2 = thread::spawn(move || {
thread::sleep(duration);
println!("线程2 {:?}", handle2_data);
println!("线程2 执行完成");
});
// 等待线程结束
handle1.join().unwrap();
handle2.join().unwrap();
println!("程序 执行结束");
}
// 程序运行结果
// 线程2 ["hello", "rust"]
// 线程2 执行完成
// 线程1 ["hello", "rust"]
// 线程1 执行完成
// 程序 执行结束
我们调用 data.clone 方法时,并不会再次创建一个 data 副本,而是增加了一次引用计数。只要有任何线程拥有 Arc<Vec>,映射就不会释放,即使是父线程已经不存在了。由于 Arc 中的数据是不可变的,所以不会出现数据竞争的问题
JoinHandle
std::thread::spawn 的返回值是 JoinHandle , 它是一个结构体,包含一个 JoinInner 类型
pub struct JoinHandle<T>(JoinInner<'static, T>);
/// Inner representation for JoinHandle
struct JoinInner<'scope, T> {
native: imp::Thread,
thread: Thread,
packet: Arc<Packet<'scope, T>>,
}
JoinHandle 用于等待线程完成其执行并检索它所产生的结果。它里面有几个重要的方法简单介绍下:
Join
要等待线程完成并获取其结果,可以调用 JoinHandle 的 join 方法(上面已经介绍过了)。该方法会阻塞当前线程,直到与 JoinHandle 关联的线程完成其执行,然后返回线程产生的结果。如果线程发生错误(panic),则 join 方法也会抛出错误(panic)。 另外子线程的返回结果也会通过 join 方法返回
fn main() {
let duration = Duration::from_millis(1000);
let handle = thread::spawn(move || {
thread::sleep(duration);
// 返回一个 String 类型
return "hello".to_string();
});
// 父线程接收子线程运行结束的结果
let result = handle.join().unwrap();
println!("{}", result);
}
// 运行结果
// hello
is_finished
该方法从 Rust 1.61.0 版本变为 stable。该方法用于检查与 JoinHandle 相关联的线程是否已经执行完成。通常情况下,在调用 join 方法之前,我们可以使用 is_finished 方法来查询与 JoinHandle 对象相关联的线程是否已经完成执行。如果返回值为 true,则说明线程已经执行完成,并且可以通过调用 join 方法来等待线程退出并获取其返回值。
fn main() {
let duration = Duration::from_millis(1000);
let handle = thread::spawn(move || {
thread::sleep(duration);
// 返回一个 String 类型
return "hello".to_string();
});
let handle_finished = handle.is_finished();
println!("handle_finished: {}", handle_finished);
// 父线程接收子线程运行结束的结果
let result = handle.join().unwrap();
println!("{}", result);
}
// 运行结果
// handle_finished: false
// hello
Thread
我们可以使用 thread 方法从 JoinHandle 中获取线程的一些信息,像线程 Id, 线程的名称等。示例代码如下:
fn main() {
let duration = Duration::from_millis(1000);
let handle = thread::spawn(move || {
thread::sleep(duration);
// 返回一个 String 类型
return "hello".to_string();
});
let thread = handle.thread();
println!("{:?}", thread);
// 父线程接收子线程运行结束的结果
let result = handle.join().unwrap();
println!("{}", result);
}
// 运行结果
// Thread { id: ThreadId(2), name: None, .. }
// hello
通道
通道(Channel)就是把值从一个线程发送到另一个线程的单向管道。它是一个线程安全的队列.如果把一个非 Copy 类型的值从发送线程转移到了接收线程,那么它的所有权也将转移。通过通道(Channel),Rust可以是现在线程之间通信,这也是一种相对简单的线程间的通信方式,因为它并不需要借助 锁 或者 共享内存。
mpsc 和 mpmc
在 Rust 标准库中提供了两种不同类型的通道:mpsc 和 mpmc。
mpsc 代表多个生产者单个消费者(Multi-producer, Single-consumer)通道。
该通道一般是由FIFO的队列来实现。允许有多个线程可以同时将消息发送到通道,但只有一个线程可以从中接收消息。其中生产者和消费者都可以非阻塞地发送和接收消息。如果通道缓冲区已满,则发送操作会阻塞,直到有空间可用。反之如果通道为空,则接收操作会阻塞,直到有通道内有消息产生。
mpmc 代表多个生产者多个消费者(Multi-producer Multi-consumer)通道。
允许多个线程可以同时将消息发送到通道,并且多个线程可以同时从中接收消息。允许生产者和消费者同时进行非阻塞的发送和接收操作。如果通道缓冲区已满,则发送操作会阻塞,直到有空间可用。类似地,如果通道为空,则接收操作会阻塞,直到有消息可用。因此,mpsc 适用于单个消费者的场景,而 mpmc 适用于多个消费者的场景。
我们通过 std::sync::mpsc 中的 channel 函数创建一个通道。源码如下:
// std::sync::mpsc
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
let (tx, rx) = mpmc::channel();
(Sender { inner: tx }, Receiver { inner: rx })
}
#[must_use]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
let (tx, rx) = mpmc::channel();
(Sender { inner: tx }, Receiver { inner: rx })
}
// std::sync::mpmc
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
let (s, r) = counter::new(list::Channel::new());
let s = Sender { flavor: SenderFlavor::List(s) };
let r = Receiver { flavor: ReceiverFlavor::List(r) };
(s, r)
}
enum SenderFlavor<T> {
/// Bounded channel based on a preallocated array.
Array(counter::Sender<array::Channel<T>>),
/// Unbounded channel implemented as a linked list.
List(counter::Sender<list::Channel<T>>),
/// Zero-capacity channel.
Zero(counter::Sender<zero::Channel<T>>),
}
创建通道
我们本文介绍的是mpsc类型的通道,通道保存的数据是有类型的,所以 channel 函数会有一个泛型。
use std::sync::mpsc::channel;
fn main() {
let (sender,receiver) = channel::<String>();
}
上面的代码表示,创建一个传输 String 类型的通道,channel 函数返回一个元组,分别是消息发送者和接收者。channel 底层的实现是一个链表(Linked List)
发送与接收
发送值
fn main() {
let (sender, receiver) = channel::<String>();
let handle1 = thread::spawn(move || {
sleep(Duration::from_millis(1000));
let hello = "hello".to_string();
// 发送者的所有权转移至线程内,发送 hello 字符串
sender.send(hello).unwrap();
});
handle1.join().unwrap();
}
首先我们创建一个线程,在线程中延迟1s后通过通道发送 hello 字符串。这时候 hello 字符串被转移到通道的缓冲区内。hello 被发送后,其所有权也相应的被转移
接收者
代码如下:
fn main {
// ... 省略发送者代码
let handle2 = thread::spawn(move || {
// 接收者接收 hello 字符串
let receive_hello = receiver.recv().unwrap();
println!("receive_hello: {}", receive_hello);
});
handle2.join().unwrap();
// ...
}
// 运行结果
// receive_hello: hello
接收者,通过 receiver 中的 recv 方法来接收通道中的值。接收者是可以迭代的,上面的接收代码可以写成下面这样:
receiver.into_iter().for_each(|item| {
println!("receive_hello: {}", item);
});
另外,接收者的 recv 方法是阻塞的,如果等不到发送者发送消息,那么它会一直阻塞线程。接收者会在通道为空并且发送者 Sender 被清除时正常退出。在上面的代码中,线程 handle1 的发送者发送字符串结束后,闭包也结束,线程退出。线程 handle2 接收者接收完成数据后,通道缓冲区为空,符合正常退出的情况,则不会一直阻塞线程。
多个发送者
channel 函数返回的元组是 (Sender, Receiver),其中 Sender 实现了 Clone 类型,但是 Receiver 没有实现 Clone 类型。所以。要获得一个拥有多个发送者的通道,只需要创建一个常规的通道,然后克隆多个 Sender,需要多少就克隆多少,最后再将 Sender 转移至不同的线程。只要其中有一个 Sender 没有被销毁,那么Receiver 就将会一直阻塞
fn main() {
let (sender, receiver) = channel::<String>();
let sender1 = sender.clone();
// let sender2 = sender.clone();
let handle1 = thread::spawn(move || {
sleep(Duration::from_millis(1000));
let hello = "hello".to_string();
sender1.send(hello).unwrap();
});
let handle2 = thread::spawn(move || {
sleep(Duration::from_millis(1000));
let rust = "rust".to_string();
sender.send(rust).unwrap();
});
let handle3 = thread::spawn(move || {
// 迭代
receiver.into_iter().for_each(|item| {
println!("receive_hello: {}", item);
});
});
handle1.join().unwrap();
handle2.join().unwrap();
handle3.join().unwrap();
}
// 运行结果
// receive_hello: hello
// receive_hello: rust
如果我们克隆一个 sender2,将线程 handle2 中的 sender 替换为 sender2 ,那 handle3 中的 receiver 将会一直阻塞
异步通道
之前我们使用的都是异步通道:无论接收者是否正在接收消息,消息发送者在发送消息时都不会阻塞:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx)= mpsc::channel();
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
主线程因为睡眠阻塞了 3 秒,因此并没有进行消息接收,而子线程却在此期间轻松完成了消息的发送。等主线程睡眠结束后,才姗姗来迟的从通道中接收了子线程老早之前发送的消息。
从输出还可以看出,发送之前和发送之后是连续输出的,没有受到接收端主线程的任何影响,因此通过mpsc::channel创建的通道是异步通道。
同步通道
与异步通道相反,同步通道发送消息是阻塞的,只有在消息被接收后才解除阻塞,例如:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx)= mpsc::sync_channel(0);
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
发送之后
可以看出,主线程由于睡眠被阻塞导致无法接收消息,因此子线程的发送也一直被阻塞,直到主线程结束睡眠并成功接收消息后,发送才成功:发送之后的输出是在receive 1之后,说明只有接收消息彻底成功后,发送消息才算完成。
消息缓存
细心的读者可能已经发现在创建同步通道时,我们传递了一个参数0: mpsc::sync_channel(0);该值可以用来指定同步通道的消息缓存条数,当你设定为N时,发送者就可以无阻塞的往通道中发送N条消息,当消息缓冲队列满了后,新的消息发送将被阻塞(如果没有接收者消费缓冲队列中的消息,那么第N+1条消息就将触发发送阻塞)。
关闭通道
之前我们数次提到了通道关闭,并且提到了当通道关闭后,发送消息或接收消息将会报错。那么如何关闭通道呢? 很简单:所有发送者被drop或者所有接收者被drop后,通道会自动关闭。
神奇的是,这件事是在编译期实现的,完全没有运行期性能损耗!只能说 Rust 的Drop特征 YYDS!
传输多种类型的数据
之前提到过,一个消息通道只能传输一种类型的数据,如果你想要传输多种类型的数据,可以为每个类型创建一个通道,你也可以使用枚举类型来实现:
use std::sync::mpsc::{self, Receiver, Sender};
enum Fruit {
Apple(u8),
Orange(String)
}
fn main() {
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),
}
}
}
如上所示,枚举类型还能让我们带上想要传输的数据,但是有一点需要注意,Rust 会按照枚举中占用内存最大的那个成员进行内存对齐,这意味着就算你传输的是枚举中占用内存最小的成员,它占用的内存依然和最大的成员相同, 因此会造成内存上的浪费。
新手容易遇到的坑
mpsc虽然相当简洁明了,但是在使用起来还是可能存在坑:
use std::sync::mpsc;
fn main() {
use std::thread;
let (send, recv) = mpsc::channel();
let num_threads = 3;
for i in 0..num_threads {
let thread_send = send.clone();
thread::spawn(move || {
thread_send.send(i).unwrap();
println!("thread {:?} finished", i);
});
}
// 在这里drop send...
for x in recv {
println!("Got: {}", x);
}
println!("finished iterating");
}
以上代码看起来非常正常,但是运行后主线程会一直阻塞,最后一行打印输出也不会被执行,原因在于: 子线程拿走的是复制后的send的所有权,这些拷贝会在子线程结束后被drop,因此无需担心,但是send本身却直到main函数的结束才会被drop。
之前提到,通道关闭的两个条件:发送者全部drop或接收者被drop,要结束for循环显然是要求发送者全部drop,但是由于send自身没有被drop,会导致该循环永远无法结束,最终主线程会一直阻塞。
解决办法很简单,drop掉send即可:在代码中的注释下面添加一行drop(send);。
mpmc 更好的性能
如果你需要 mpmc(多发送者,多接收者)或者需要更高的性能,可以考虑第三方库:
- crossbeam-channel, 老牌强库,功能较全,性能较强,之前是独立的库,但是后面合并到了
crossbeam主仓库中 - flume, 官方给出的性能数据某些场景要比 crossbeam 更好些