想想,有一个需要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:Fn、FnMut 和 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,
{
}
}
现在我们有了一个有效的线程池线程数量,我们现在要考虑储存在哪里。
我们可以再次观察spawn,spawn 返回 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,日志里区分每个线程
我们接下来要做的就是:
- 定义
Worker结构体存放id和JoinHandle<()> - 修改
ThreadPool存放一个Worker实例的 vector - 定义
Worker::new函数,它获取一个id数字并返回一个带有id和用空闭包分配的线程的Worker实例 - 在
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 实例发送任务
ThreadPool会创建一个信道并充当发送端。- 每个
Worker将会充当信道的接收端。 - 新建一个
Job结构体来存放用于向信道中发送的闭包。 execute方法会在信道发送端发出期望执行的任务。- 在线程中,
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了。