[rust]多线程通信之通道

介绍

Rust标准库提供了多种用于线程间通信和同步的工具,主要包括通道(channels)、互斥锁(Mutexes)、读写锁(RwLock)、条件变量(Condvar)以及基于原子操作的类型。

其中最常用的是多线程通道(channel),通道允许线程之间发送和接收消息,从而实现数据的传递和同步。

rust 复制代码
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("Hello from thread!");
        tx.send(val).unwrap();
        // 在调用send时,变量的所有权被转移了
        // println!("val: {}",val),打开后编译会报错 value borrowed here after move
    });

    println!("Received: {}", rx.recv().unwrap());
}

知识点:

  1. 发送者的send方法返回的是一个Result<T,E>
  2. 接受者的recv方法返回的也是一个Result<T,E>,当通道发送端关闭时,返回一个错误值
  3. 接受者的recv方法会阻塞直到有消息到来,也可以使用try_recv(),不阻塞的方法,会立即返回
  4. 约定俗成的是,通常用变量 tx 表示发送端,rx 表示接收端
  5. mpsc 表示多个生产者,单个接收者,与之对应的有 spmc(single-producer, multiple-consumer),表示单个生产者,多个接受者,但Rust 标准库中并没有直接提供 spmc 通道

发送多个值

rust 复制代码
use std::sync::mpsc;
use std::thread;
use std::time::{Duration};
use chrono::Local;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vec = vec![
            String::from("Hello"),
            String::from("from"),
            String::from("thread"),
            String::from("!"),
        ];
        for v in vec {
            tx.send(v).unwrap();
            thread::sleep(Duration::from_secs(1)); // 休眠一秒
        }
    });

    for rv in rx { // 发送多个值
        // `Local::now()`返回当前的本地时间
        // 使用`format`方法将时间格式化成指定的字符串格式
        println!("[{:?}] Received: {}", Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), rv);
    }
}

// 输出:
// ["2024-10-05 14:43:51"] Received: Hello
// ["2024-10-05 14:43:52"] Received: from
// ["2024-10-05 14:43:53"] Received: thread
// ["2024-10-05 14:43:54"] Received: !
// 
// 进程已结束,退出代码为 0

知识点:

  1. 在Rust中获取和打印当前时间可以通过chrono这个流行的第三方库来实现。chrono提供了丰富的日期和时间处理功能,包括获取当前时间并以多种格式打印。
  2. Rust的标准库提供了与时间相关的功能,通过std::time模块可以获取系统时间戳并进行一些基本操作。不过,标准库并没有直接提供像chrono那样的丰富的日期和时间功能,主要是处理时间点(std::time::SystemTime)和时间间隔(std::time::Duration
ini 复制代码
[dependencies]
chrono = "0.4.38" 

多个生产者

rust 复制代码
use std::sync::mpsc;
use std::thread;
use std::time::{Duration};

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    // 克隆多个发送者
    let tx1 = tx.clone();
    let tx2 = tx.clone();

    // 和上面的方式等效
    /* let tx1 = mpsc::Sender::clone(&tx);
     let tx2 = mpsc::Sender::clone(&tx);*/

    // 生产者1
    thread::spawn(move || {
        let messages = vec![
            String::from("消息1 from 生产者1"),
            String::from("消息2 from 生产者1"),
        ];

        for msg in messages {
            tx1.send(msg).unwrap();
            thread::sleep(Duration::from_millis(1000));
        }
        
        // 作用域结束,tx1句柄将被隐式丢弃
    });

    // 生产者2
    thread::spawn(move || {
        let messages = vec![
            String::from("消息1 from 生产者2"),
            String::from("消息2 from 生产者2"),
        ];

        for msg in messages {
            tx2.send(msg).unwrap();
            thread::sleep(Duration::from_millis(500));
        }
        
        // 作用域结束,tx2句柄将被隐式丢弃
    });

    // 消费者
    for received in rx {
        println!("收到: {}", received);
    }
}

// 输出:
// 收到: 消息1 from 生产者1
// 收到: 消息1 from 生产者2
// 收到: 消息2 from 生产者2
// 收到: 消息2 from 生产者1

知识点:

  1. 使用mpsc::channel()函数创建一个新的通道,返回发送者(tx)和接收者(rx)句柄
  2. 通过调用tx.clone()克隆出多个发送者句柄,这样不同的生产者线程可以各自持有一个发送者句柄
  3. 消费者在主线程中运行,通过遍历rx来接收消息。变量rx是一个Receiver,实现了Iterator,因此可以直接在for循环中使用
  4. 当发送者句柄超出其作用域时,Rust 会自动进行垃圾回收,释放资源。这会导致发送者句柄被丢弃,进而关闭通道。tx1 和 tx2 就在线程执行结束之后被隐式关闭
  5. std::sync::mpsc通道会在所有发送者关闭后自动关闭,这个例子中因为 tx 没有被关闭,所以会主线程会被一直阻塞
  6. std::sync::mpsc通道的发送端被关闭后,接收端仍然可以接收到在通道关闭前发送的所有消息。换句话说,通道一旦关闭,发送端不再能发送新的消息,但接收端仍然可以继续接收已经在通道中的消息,直到通道中没有更多消息为止。

发送端显示关闭通道

scss 复制代码
fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    // 克隆多个发送者
    let tx1 = tx.clone();
    let tx2 = tx.clone();
    
    // 显式关闭通道
    drop(tx);
}

超时等待

rust 复制代码
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::{Duration};

fn main() {
    let (tx, rx) = mpsc::channel();

    // 生产者线程发送消息
    thread::spawn(move || {
        thread::sleep(Duration::from_millis(1500));  // 模拟一些耗时操作
        tx.send(String::from("消息 from 生产者")).unwrap();
    });

    // 消费者设置1秒超时接收消息
    receive(&rx);
    // 再等待一秒,可以拿到通道中的消息了
    receive(&rx);
    // 又等了一秒,此时线程应该执行结束,通过被隐私关闭,发送者已经断开连接
    receive(&rx);
}

fn receive(rx: &Receiver<String>) {
    match rx.recv_timeout(Duration::from_secs(1)) {
        Ok(message) => println!("收到: {}", message),
        Err(mpsc::RecvTimeoutError::Timeout) => println!("接收消息超时!"),
        Err(mpsc::RecvTimeoutError::Disconnected) => println!("发送者已经断开连接!"),
    }
}

// 输出:
// 接收消息超时!
// 收到: 消息 from 生产者
// 发送者已经断开连接!
// 
// 进程已结束,退出代码为 0

使用场景:

  1. 防止无限阻塞:在某些情况下,接收者线程可能会无限等待消息,从而导致程序无法继续执行其他任务。超时等待可以防止这种情况,确保线程在一定时间内没有收到消息时可以继续执行其他操作或进行适当的错误处理。
  2. 周期性任务:在某些应用中,可能需要定期检查某些状态或者执行某些任务。这时候使用超时等待能让线程在设定的间隔时间内执行这些检查或任务。
  3. 用户交互和响应:可以在等待输入或请求的同时执行其他重要任务或检查是否有其他事件需要处理。
  4. 网络请求超时:在基于网络的应用中,可能需要等待来自远程服务器的响应。使用超时等待,可以防止网络请求因为各种原因挂起太久,从而影响程序的正常运行。

接收端断开

如果通道的接收端断开,即接收端被关闭或掉线,发送端在尝试发送消息时会遇到 SendError错误。表明消息无法发送因为没有接收者存在。需要在发送端处理这个错误,以确保程序能正确处理这种异常情况。

rust 复制代码
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::{Duration};

fn main() {
    let (tx, rx) = mpsc::channel();

    // 创建接收者线程
    let receiver_handle = thread::spawn(move || {
        // 接收消息并打印出来
        match rx.recv_timeout(Duration::from_secs(2)) {
            Ok(message) => println!("接收到: {}", message),
            Err(_) => println!("未收到消息或接收超时"),
        }
        // 接收端在这里结束,模拟接收端断开
    });

    // 让主线程等待一秒,确保接收端已经开始等待消息
    thread::sleep(Duration::from_secs(1));

    // 创建发送者线程
    let sender_handle = thread::spawn(move || {
        thread::sleep(Duration::from_secs(3)); // 模拟发送操作的延迟
        match tx.send("消息 from 发送者") {
            Ok(_) => println!("消息发送成功"),
            Err(e) => println!("消息发送失败: {}", e),
        }
    });

    // 等待接收者线程完成
    receiver_handle.join().unwrap();
    // 等待发送者线程完成
    sender_handle.join().unwrap();
}

fn receive(rx: &Receiver<String>) {
    match rx.recv_timeout(Duration::from_secs(1)) {
        Ok(message) => println!("收到: {}", message),
        Err(mpsc::RecvTimeoutError::Timeout) => println!("接收消息超时!"),
        Err(mpsc::RecvTimeoutError::Disconnected) => println!("发送者已经断开连接!"),
    }
}

// 输出:
// 未收到消息或接收超时
// 消息发送失败: sending on a closed channel
// 
// 进程已结束,退出代码为 0

spmc

Rust 标准库中并没有直接提供 spmc 通道,我们可以使用 crossbeam crate,其中已经实现了更高级的通道,包括 spmc

ini 复制代码
[dependencies]
crossbeam = "0.8.4"

示例

rust 复制代码
use std::thread;
use std::time::{Duration};
use crossbeam::channel;

fn main() {
    // 创建一个有界的 spmc 通道
    let (sender, receiver) = channel::bounded(5);

    // 创建并启动生产者线程
    let producer_handle = thread::spawn(move || {
        for i in 1..=3 {
            thread::sleep(Duration::from_millis(500)); // 模拟生产延迟
            match sender.send(i) {
                Ok(_) => println!("生产者: 发送消息 {}", i),
                Err(err) => println!("生产者: 发送失败 {}", err),
            }
        }
        // 关闭发送端,表示不再发送消息
        drop(sender);
    });

    // 启动多个消费者线程
    let mut consumer_handles = Vec::new();
    for id in 0..5 {
        let receiver_clone = receiver.clone();
        let handle = thread::spawn(move || {
            while let Ok(msg) = receiver_clone.recv() {
                println!("消费者 {}: 收到消息 {}", id, msg);
            }
            println!("消费者 {}: 结束", id);
        });
        consumer_handles.push(handle);
    }

    // 等待生产者线程完成
    producer_handle.join().expect("生产者线程崩溃");

    // 等待所有消费者线程完成
    for handle in consumer_handles {
        handle.join().expect("消费者线程崩溃");
    }
}

// 输出:
// 生产者: 发送消息 1
// 消费者 0: 收到消息 1
// 生产者: 发送消息 2
// 消费者 2: 收到消息 2
// 生产者: 发送消息 3
// 消费者 1: 收到消息 3
// 消费者 1: 结束
// 消费者 3: 结束
// 消费者 4: 结束
// 消费者 0: 结束
// 消费者 2: 结束
// 
// 进程已结束,退出代码为 0

知识点:

  1. channel::bounded(5) 创建一个有界的(最多可存储 5 条消息)spmc 通道,返回发送者和接收者。好比是一个消费组被一群消费者乱序消费,总的3条消息,最多只能被3个消费者消费到,其余消费者空闲
  2. 主线程等待生产者线程完成后再等待所有消费者线程完成。使用 join 方法确保线程顺利结束。
相关推荐
fqbqrr3 小时前
2411rust,实现特征
rust
码农飞飞3 小时前
详解Rust枚举类型(enum)的用法
开发语言·rust·match·枚举·匹配·内存安全
一个小坑货9 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet279 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
VertexGeek11 小时前
Rust学习(八):异常处理和宏编程:
学习·算法·rust
前端与小赵1 天前
什么是Sass,有什么特点
前端·rust·sass
一个小坑货1 天前
Rust基础
开发语言·后端·rust
Object~1 天前
【第九课】Rust中泛型和特质
开发语言·后端·rust
码农飞飞2 天前
详解Rust结构体struct用法
开发语言·数据结构·后端·rust·成员函数·方法·结构体
Dontla2 天前
Rust derive macro(Rust #[derive])Rust派生宏
开发语言·后端·rust