使用 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. 多个线程同步发送
相关推荐
后端码匠31 分钟前
Spring Boot3+Vue2极速整合:10分钟搭建DeepSeek AI对话系统
人工智能·spring boot·后端
guyoung43 分钟前
DeepSeek轻量级本地化部署工具——AIMatrices DeepSeek
rust·llm·deepseek
可乐张1 小时前
AutoGen 技术博客系列 (九):从 v0.2 到 v0.4 的迁移指南
后端·llm
计算机-秋大田1 小时前
基于Spring Boot的农产品智慧物流系统设计与实现(LW+源码+讲解)
java·开发语言·spring boot·后端·spring·课程设计
计算机毕设指导61 小时前
基于SpringBoot的城乡商城协作系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·maven
华子w9089258591 小时前
基于数据可视化+SpringBoot+安卓端的数字化施工项目计划与管理平台设计和实现
java·spring boot·后端
橘猫云计算机设计1 小时前
基于Django的购物商城平台的设计与实现(源码+lw+部署文档+讲解),源码可白嫖!
java·数据库·spring boot·后端·django
2501_903238651 小时前
Spring Boot日志配置与环境切换实战
数据库·spring boot·后端·个人开发
WeiLai11122 小时前
面试基础--微服务架构:如何拆分微服务、数据一致性、服务调用
java·分布式·后端·微服务·中间件·面试·架构
猿java3 小时前
很多程序员会忽略的问题:创建 MySQL索引,需要注意什么?
java·后端·mysql