使用 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 个客户端连接

线程池创建

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

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

给这个结构体创建两个关联函数 new 和 execute,new 用于创建一个新的线程池,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! 中写任何可能会影响程序逻辑的代码

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

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 可以被克隆为 sender1 和 sender2,然用不同的线程发送消息,在 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 是一个工作线程,有两个属性 id 和 thread,id 是线程的标识,thread 是一个 Option<thread::JoinHandle<()>>,用于存放线程句柄

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

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

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

消息类型 Message 是一个枚举类型,有两个成员 NewJob 和 Terminate,NewJob 用于接收新的任务,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 trait 的 drop 方法

遍历所有 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. 多个线程同步发送
相关推荐
用户98883218717424 分钟前
别把 IOptionsSnapshot 注入单例:从生命周期看 .NET 配置读取
后端
姜鱼问生26 分钟前
别再 chmod 777 了,用 ACL 给它开一扇窗
后端·算法·架构
仿生joe会梦见漫天的大雪吗30 分钟前
CTF Misc 工具选型指南:按题型分类的工具、原理与限制
后端·安全
南丘山30 分钟前
一个 VARCHAR(1) 的字段,卡住了整条 DTU 数据链路
后端
南丘山33 分钟前
多租户隔离失效,我踩过的 3 个坑
后端
IT_陈寒34 分钟前
Java缓存踩坑记:明明设置了过期时间,怎么还是读到脏数据?
前端·人工智能·后端
用户86629784800835 分钟前
数据库报错"坏块"别急着删库重来,试试 KingbaseES 的离线修复工具
后端
jose37 分钟前
URL 里多写了一个端口,为什么请求还能调通?
java·后端
用户83562907805139 分钟前
使用 Python 在 Excel 中识别并高亮重复数据与唯一数据
后端·python
CCblandy40 分钟前
Agent 工具调用怎么编排?本机实测:并行只到 2.63 倍,慢的先发省 30ms,CPU 活加 worker 无效
人工智能·后端·python·性能优化