Rust-异步-线程与通道

128 阅读12分钟

线程

线程的创建与使用

创建线程通常被称为 产生线程 (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之后,说明只有接收消息彻底成功后,发送消息才算完成

消息缓存

细心的读者可能已经发现在创建同步通道时,我们传递了一个参数0mpsc::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,会导致该循环永远无法结束,最终主线程会一直阻塞。

解决办法很简单,dropsend即可:在代码中的注释下面添加一行drop(send);

mpmc 更好的性能

如果你需要 mpmc(多发送者,多接收者)或者需要更高的性能,可以考虑第三方库:

  • crossbeam-channel, 老牌强库,功能较全,性能较强,之前是独立的库,但是后面合并到了crossbeam主仓库中
  • flume, 官方给出的性能数据某些场景要比 crossbeam 更好些