使用 rust 创建多线程 http-server

rust 编写一个 http 服务器

rust 复制代码
fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    let pool = ThreadPool::new(4);
    for stream in listener.incoming().take(4) {
        let stream = stream.unwrap();
        pool.execute(move || handle_client(stream));
    }
    Ok(())
}

listener.incoming() 返回一个迭代器,可以持续不断地接受新的 TCP 连接。这个迭代器理论上是无限的,会一直等待并接受新的连接

.take(4) 是对这个迭代器的限制操作,最多接受 4 个客户端连接

线程池创建

创建一个线程池,有两个属性 workerssenderworkers 是一个 Vec,存放所有的工作线程,sender 是一个 mpsc::Sender<Message>,用于发送任务给工作线程

rust 复制代码
pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

给这个结构体创建两个关联函数 newexecutenew 用于创建一个新的线程池,execute 用于向线程池中的工作线程发送任务

rust 复制代码
impl ThreadPool {
    pub fn new(size: usize) -> Self {
        assert!(size > 0);
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

assert!

assert! 这个宏,主要用于开发和测试阶段,断言失败会导致程序 panic

release 模式下,assert! 会被移除,不会对程序产生任何影响,所以不要在 assert! 中写任何可能会影响程序逻辑的代码

在生产环境中,应该使用适当的错误处理机制(如 ResultOption

mpsc

mpsc (Multiple Producer, Single Consumer) 是 Rust 标准库提供的一个多生产者单消费者通道

rust 复制代码
let (sender, receiver) = mpsc::channel();
  • sender: 发送端,可以克隆(多个生产者)
  • receiver: 接收端,不能克隆(单个消费者)
  1. 基本使用
rust 复制代码
fn main() {
    // 创建一个通道,返回发送者和接收者
    let (sender, receiver) = mpsc::channel();

    // 创建一个新线程
    thread::spawn(move || {
        let messages = vec!["你好", "世界", "!"];

        // 发送端发送数据
        for msg in messages {
            sender.send(msg).unwrap();
        }
    });

    // 主线程接收数据
    for received in receiver {
        println!("收到: {}", received);
    }
}
  1. 多线程发送者,sender 可以被克隆为 sender1sender2,然用不同的线程发送消息,在 receiver 中接收消息
rust 复制代码
fn main() {
    let (sender, receiver) = mpsc::channel();

    // 克隆发送者
    let sender1 = sender.clone();
    let sender2 = sender.clone();

    // 线程1
    thread::spawn(move || {
        sender1.send("来自线程1").unwrap();
    });

    // 线程2
    thread::spawn(move || {
        sender2.send("来自线程2").unwrap();
    });

    // 原始发送者
    sender.send("来自主线程").unwrap();

    // 接收消息
    for _ in 0..3 {
        println!("{}", receiver.recv().unwrap());
    }
}
  1. 同步发送

使用 sync_channel 创建一个同步通道,接收一个缓冲区,当缓冲区满时,生产者会被阻塞直到有空间

rust 复制代码
fn main() {
    // 创建一个容量为 2 的缓冲通道
    let (sender, receiver) = mpsc::sync_channel(2);

    // 生产者线程
    thread::spawn(move || {
        for i in 1..=5 {
            println!("生产者: 正在发送数据 {}", i);
            sender.send(i).unwrap();
            println!("生产者: 数据 {} 已发送", i);
        }
    });

    // 消费者线程故意慢一点处理
    thread::spawn(move || {
        for msg in receiver {
            println!("消费者: 收到数据 {}", msg);
            // 模拟处理数据的耗时
            thread::sleep(Duration::from_secs(1));
        }
    });

    // 让主线程等待一会
    thread::sleep(Duration::from_secs(6));
}
  1. 多线程同步发送
rust 复制代码
fn main() {
    // 创建一个容量为 2 的缓冲通道
    let (sender, receiver) = mpsc::sync_channel(2);

    // 创建多个生产者
    for id in 1..=3 {
        let sender = sender.clone();
        thread::spawn(move || {
            for i in 1..=3 {
                let data = format!("生产者{}-数据{}", id, i);
                println!("{}: 准备发送", data);
                sender.send(data.clone()).unwrap();
                println!("{}: 已发送", data);
                thread::sleep(Duration::from_millis(1500));
            }
        });
    }

    // 丢弃原始sender
    drop(sender);

    // 消费者
    let consumer = thread::spawn(move || {
        for received in receiver {
            println!("消费者: 正在处理 {}", received);
            thread::sleep(Duration::from_secs(1));
            println!("消费者: 处理完成 {}", received);
        }
    });

    // 等待消费者处理完所有数据
    consumer.join().unwrap();
}
  1. 当使用了 sender.clone() 后需要显示调用 drop(sender),否则 receiver 会一直等待

execute

execute 方法接受一个闭包,将其包装为 Box,然后发送给工作线程

这个闭包的类型是 F,它是一个泛型参数,有三个约束条件

  1. FnOnce(): 闭包没有参数,没有返回值
    • FnOnce 表示这个函数在执行时会消耗掉自己(只能调用一次)
    • () 表示这个函数没有参数
  2. Send: 闭包可以跨线程传递
    • 这是 Rust 并发安全的一个重要特质
    • 允许这个值在线程间安全移动所有权
  3. 'static: 闭包的生命周期是静态的
    • 意味着这个值可以存活任意长的时间
    • 通常用于需要长期存储或在线程间传递的值
rust 复制代码
pub fn execute<F>(&self, f: F)
where
    F: FnOnce() + Send + 'static,
{
    let job = Box::new(f);
    self.sender.send(Message::NewJob(job)).unwrap();
}

这里 where 的意思是泛型约束,等同于

rust 复制代码
pub fn execute<F: FnOnce() + Send + 'static>(&self, f: F) {
    let job = Box::new(f);
    self.sender.send(Message::NewJob(job)).unwrap();
}

创建工作线程

Worker 是一个工作线程,有两个属性 idthreadid 是线程的标识,thread 是一个 Option<thread::JoinHandle<()>>,用于存放线程句柄

rust 复制代码
struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

Worker 创建一个关联函数 new,用于创建一个新的工作线程

因为 receiver 是个 Mutex 类型,所以需要调用 lock 方法获取锁,然后调用 recv 方法接收消息

消息类型 Message 是一个枚举类型,有两个成员 NewJobTerminateNewJob 用于接收新的任务,Terminate 用于终止线程

rust 复制代码
impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv().unwrap();
            match message {
                Message::NewJob(job) => {
                    println!("Worker {} receive a job.", id);
                    job();
                }
                Message::Terminate => {
                    println!("Worker {} receive terminate.", id);
                    break;
                }
            }
        });
        Worker {
            id,
            thread: Some(thread),
        }
    }
}

发送消息的消息的格式 Message::NewJob(job),结束表示的消息格式 Message::Terminate

rust 复制代码
type Job = Box<dyn FnOnce() + Send + 'static>;

enum Message {
    NewJob(Job),
    Terminate,
}

通过 match 匹配消息类型,如果是 NewJob 类型,就执行闭包,如果是 Terminate 类型,就退出循环

Drop

线程池创建后,如何优雅的关闭线程呢

rust 提供了一个 Drop trait 用于在值离开作用域时执行清理工作

我们给 ThreadPool 实现 Drop trait,当线程池离开作用域时,会自动调用 Drop traitdrop 方法

遍历所有 workers,向 sender 发送 Terminate 消息,然后等待所有线程结束,最后调用 join 方法等待线程结束

rust 复制代码
impl Drop for ThreadPool {
    fn drop(&mut self) {
        for _ in &mut self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }

        for worker in &mut self.workers {
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

这里要注意的是这两个 for 循环不能合并,因为合并后,可能会出现这种情况:

  • 发送一个终止信号
  • 立即等待该线程结束
  • 但其他线程还没收到终止信号

这就有可能导致死锁,所以这里必须保持两个独立的循环

源码

  1. http-server-thread-pool
  2. mpsc::sync_channel 使用
    1. 基本使用
    2. 多个发送者
    3. 同步发送
    4. 多个线程同步发送
相关推荐
平凡的运维之路12 分钟前
vsftpd虚拟用户部署
后端
叫我:松哥35 分钟前
基于Python django的音乐用户偏好分析及可视化系统设计与实现
人工智能·后端·python·mysql·数据分析·django
Leaf吧3 小时前
springboot 配置多数据源以及动态切换数据源
java·数据库·spring boot·后端
代码驿站5203 小时前
JavaScript语言的软件工程
开发语言·后端·golang
Archy_Wang_14 小时前
ASP.NET Core 中的 JWT 鉴权实现
后端·ui·asp.net
Archy_Wang_14 小时前
ASP.NET Core中 JWT 实现无感刷新Token
后端·asp.net
m0_748230944 小时前
SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
spring boot·后端·pdf
好像是个likun4 小时前
spring Ioc 容器的简介和Bean之间的关系
java·后端·spring
计算机-秋大田5 小时前
基于微信小程序的电子点菜系统设计与实现(KLW+源码+讲解)
java·后端·微信小程序·小程序·课程设计