rust构建多线程webserver(2)

92 阅读10分钟

想想,有一个需要5秒的访问,100个用户都会访问需要多久。第100个访问的用户肯定很不服气,为什么我要排500秒。所以,单线程对于不耐心的用户并不友好。

于是我们想了个办法!

使用线程池改善吞吐量

线程池thread pool)是一组预先分配的等待或准备处理任务的线程。就像厨房的厨师做菜,之前我们使用了一个厨师,现在,我们有一个厨师池,有好几个厨师,空闲的厨师就可以解决新点的菜,厨师一起做饭,客人就等的时间少。

当然我们肯定也不会为每一个请求(客人)都派一个专门的厨师,厨房的大小是有限的。

这个设计仅仅是多种改善 web server 吞吐量的方法之一。其他方法有 fork/join 模型和单线程异步 I/O 模型。我们这里使用thread pool。

开始之前,思考一下,程序应该是怎样的?

不知道大家是否了解TDD(test-driven development)测试驱动开发,我们这里想了想,这里我们编写期望的代码,然后通过观察编译器的错误思考需要如何做出改变,以及完善代码。这种开发模式就是CDD(compiler-driven development)编译驱动开发。

上面也说了,我们不能为每一个请求都分配线程,但是我们可以先试试,然后完善代码。

/src/main.rs

use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();
// thread::spawn 会创建一个新线程并在其中运行闭包中的代码
        thread::spawn(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        contents.len(),
        contents
    );

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}



fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    
    // 先创建一个现在还不存在的接口,但是我们之后会创建它,threadpool可以创建4个线程
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

上面的代码肯定报错,我们根据报错信息创建不存在的struct。

/src/lib.rs

pub struct ThreadPool;

使用pub创建接口 然后在main.rs里用use hello::ThreadPool

当然还会报错,因为我们并没有实现方法,跟new函数。

所以我们继续~

pub struct ThreadPool;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
    // 既然是线程,肯定是不会小于1
        assert!(size>0);
        ThreadPool
    }
}

接下来,我们会遇到新的报错。

是因为并没有 ThreadPool 上的 execute 方法。

我们决定线程池应该有与 thread::spawn 类似的接口,同时我们将实现 execute 函数来获取传递的闭包并将其传递给池中的空闲线程执行。

我们在 ThreadPool 上定义 execute 函数来获取一个闭包参数。 既然是闭包参数,闭包作为参数时可以使用三个不同的 trait:FnFnMut 和 FnOnce

我们最后要实现类似于thread::spawn,所以我们可以观察 thread::spawn 的签名在其参数中使用了何种 bound。通过文档会看到,spawn里:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

当然我们这里只关心F,不会看返回值。考虑到 spawn 使用 FnOnce 作为 F 的 trait bound,这可能也是我们需要的,因为最终会将传递给 execute 的参数传给 spawn。因为处理请求的线程只会执行闭包一次,这也进一步确认了 FnOnce 是我们需要的 trait,这里符合 FnOnce 中 Once 的意思。

/src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    // --snip--
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

现在我们有了一个有效的线程池线程数量,我们现在要考虑储存在哪里。 我们可以再次观察spawnspawn 返回 JoinHandle<T>,其中 T 是闭包返回的类型。 /src/lib.rs

use std::thread;

pub struct ThreadPool {
// 因为我们的情况里,不会返回任何值,所以是()表示
    threads: Vec<thread::JoinHandle<()>>,
}

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);
// 这里使用with_capacity是因为预先进行分配空间,比 `Vec::new` 要稍微有效率一些
        let mut threads = Vec::with_capacity(size);

        for _ in 0..size {
            //这里创建线程
        }

        ThreadPool { threads }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

我们没有创建线程,接下来就开始创建

使用Worker

我们需要实现的是创建线程并稍后发送代码。

所以,这会在 ThreadPool 和线程间引入一个新数据类型来管理这种新行为。这个数据结构称为 Worker,这也是一个池实现中的常见概念。

我们将会储存 Worker 结构体的实例。每一个 Worker 会储存一个单独的 JoinHandle<()> 实例。

然后在 Worker 上实现一个方法,获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会给每一个 worker id,日志里区分每个线程

我们接下来要做的就是:

  1. 定义 Worker 结构体存放 id 和 JoinHandle<()>
  2. 修改 ThreadPool 存放一个 Worker 实例的 vector
  3. 定义 Worker::new 函数,它获取一个 id 数字并返回一个带有 id 和用空闭包分配的线程的 Worker 实例
  4. 在 ThreadPool::new 中,使用 for 循环计数生成 id,使用这个 id 新建 Worker,并储存进 vector 中
use std::thread;

pub struct ThreadPool {
// 将threads改为单独的worker
    workers: Vec<Worker>,
}

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool { workers }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

// 实现一下worker
struct Worker {
    id: usize,
    // 单独的线程
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
    //使用id储存使用了一个空闭包创建的 `JoinHandle<()>`。
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}

使用信道向线程发送请求

现在要解决的是传递给thread::spawn的闭包没有做任何的事情。

我们在 execute 方法中获得期望执行的闭包,不过在创建 ThreadPool 的过程中创建每一个 Worker 时需要向 thread::spawn 传递一个闭包。

我们希望刚创建的 Worker 结构体能够从 ThreadPool 的队列中获取需要执行的代码,并发送到线程中执行他们。

execute 将通过 ThreadPool 向其中线程正在寻找工作的 Worker 实例发送任务

  1. ThreadPool 会创建一个信道并充当发送端。
  2. 每个 Worker 将会充当信道的接收端。
  3. 新建一个 Job 结构体来存放用于向信道中发送的闭包。
  4. execute 方法会在信道发送端发出期望执行的任务。
  5. 在线程中,Worker 会遍历信道的接收端并执行任何接收到的任务。
use std::thread;
//Multi-producer, single-consumer FIFO queue communication primitives.

//This module provides message-based communication over channels, concretely defined among three types:

//-   [`Sender`](<> "https://doc.rust-lang.org/nightly/std/sync/mpsc/struct.Sender.html")
//-   [`SyncSender`](<> "https://doc.rust-lang.org/nightly/std/sync/mpsc/struct.SyncSender.html")
//-   [`Receiver`](<> "https://doc.rust-lang.org/nightly/std/sync/mpsc/struct.Receiver.html")
use std::sync::mpsc;

pub struct ThreadPool {
    workers: Vec<Worker>,
    //看注释
//The sending-half of Rust's asynchronous
//[`channel`](<> "https://doc.rust-lang.org/nightly/std/sync/mpsc/fn.channel.html") type.
//This half can only be owned by one thread, but it can be cloned to send to other threads.
//Messages can be sent through this channel with [`send`].
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}

在 ThreadPool::new 中,新建了一个信道,并接着让线程池在接收端等待。这段代码能够编译,不过仍有警告。

让我们尝试在线程池创建每个 worker 时将信道的接收端传递给他们。我们希望在 worker 所分配的线程中使用信道的接收端,所以将在闭包中引用 receiver 参数。 /src/lib.rs


impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

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

        ThreadPool { workers, sender }
    }
    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}
struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
//这里使用receiver
    fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

事实上这里是有问题的,这段代码尝试将 receiver 传递给多个 Worker 实例。

因为Rust 所提供的信道实现是多 生产者,单 消费者 的。

我们希望通过在所有的 worker 中共享单一 receiver,在线程间分发任务。

另外,从信道队列中取出任务涉及到修改 receiver,所以这些线程需要一个能安全的共享和修改 receiver 的方式,否则可能导致竞争状态。

这里就要用到安全智能指针了。 安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 Arc<Mutex<T>>Arc 使得多个 worker 拥有接收端,而 Mutex 则确保一次只有一个 worker 能从接收端得到任务。

use std::sync::mpsc;
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        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 {
        // 使用clone
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }


    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

struct Worker {
    id: usize,
    //这的值也是要修改的
    thread: thread::JoinHandle<Arc<Mutex<Receiver<Job>>>>,
}

impl Worker {
// 这里使用Arc
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

实现execute方法

最后让我们实现 ThreadPool 上的 execute 方法。同时也要修改 Job 结构体:它将不再是结构体,Job 将是一个有着 execute 接收到的闭包类型的 trait 对象的类型别名

use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}
//修改结构体
type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        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(job).unwrap();
    }
}
struct Worker {
    id: usize,
    thread: thread::JoinHandle<Arc<Mutex<Receiver<Job>>>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

在使用 execute 得到的闭包新建 Job 实例之后,将这些任务从信道的发送端发出。这里调用 send 上的 unwrap,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 unwrap 是因为我们知道失败不可能发生,即便编译器不这么认为。

不过到此事情还没有结束!在 worker 中,传递给 thread::spawn 的闭包仍然还只是 引用 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行他们。下面进行了修改。

use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        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(job).unwrap();
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<Arc<Mutex<Receiver<Job>>>>,
}
impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // 这里进行了修改
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("Worker {} got a job; executing.", id);

            job();
        });

        Worker { id, thread }
    }
}

完成! 接下来就可以cargo run了。