继续并对示例 20-12 中的 src∕main.rs 做出修改,并利用来自 cargo check 的编译器错误来驱动开发。下
面是我们得到的第一个错误:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type ThreadPool
–> src/main.rs:10:16
|
10 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type ThreadPool
For more information about this error, try rustc --explain E0433
.
error: could not compile hello
due to previous error
好的,这告诉我们需要一个 ThreadPool 类型或模块,所以我们将构建一个。ThreadPool 的实现会与
web server 的特定工作相独立,所以让我们从 hello crate 切换到存放 ThreadPool 实现的新库 crate。
这也意味着可以在任何工作中使用这个单独的线程池库,而不仅仅是处理网络请求。
创建 src∕lib.rs 文件,它包含了目前可用的最简单的 ThreadPool 定义:
文件名: src∕lib.rs
pub struct ThreadPool;
接着创建一个新目录,src∕bin,并将二进制 crate 根文件从 src∕main.rs 移动到 src∕bin∕main.rs。这使得
库 crate 成为 hello 目录的主要 crate;不过仍然可以使用 cargo run 运行 src∕bin∕main.rs 二进制文件。
移动了 main.rs 文件之后,修改 src∕bin∕main.rs 文件开头加入如下代码来引入库 crate 并将 ThreadPool
引入作用域:
文件名: src∕bin∕main.rs
use hello::ThreadPool;
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();
let pool = ThreadPool::new(4);
540 CHAPTER 20. 最后的项目: 构建多线程 WEB SERVER
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
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();
}
这仍然不能工作,再次尝试运行来得到下一个需要解决的错误:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named new
found for struct ThreadPool
in the current scope
–> src/bin/main.rs:11:28
|
11 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in ThreadPool
For more information about this error, try rustc --explain E0599
.
error: could not compile hello
due to previous error
这告诉我们下一步是为 ThreadPool 创建一个叫做 new 的关联函数。我们还知道 new 需要有一个参数
可以接受 4,而且 new 应该返回 ThreadPool 实例。让我们实现拥有此特征的最小化 new 函数:
文件夹: src∕lib.rs
20.2. 将单线程 SERVER 变为多线程 SERVER 541
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
这里选择 usize 作为 size 参数的类型,因为我们知道为负的线程数没有意义。我们还知道将使用 4 作为
线程集合的元素数量,这也就是使用 usize 类型的原因,如第三章 ” 整型” 部分所讲。
再次编译检查这段代码:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named execute
found for struct ThreadPool
in the current scope
–> src/bin/main.rs:16:14
|
16 | pool.execute(|| {
| ^^^^^^^ method not found in ThreadPool
For more information about this error, try rustc --explain E0599
.
error: could not compile hello
due to previous error
现在有了一个警告和一个错误。暂时先忽略警告,发生错误是因为并没有 ThreadPool 上的 execute 方
法。回忆 ” 为有限数量的线程创建一个类似的接口” 部分我们决定线程池应该有与 thread::spawn 类似
的接口,同时我们将实现 execute 函数来获取传递的闭包并将其传递给池中的空闲线程执行。
我们会在 ThreadPool 上定义 execute 函数来获取一个闭包参数。回忆第十三章的 ” 使用带有泛型和
Fn trait 的闭包” 部分,闭包作为参数时可以使用三个不同的 trait:Fn、FnMut 和 FnOnce。我们需
要决定这里应该使用哪种闭包。最终需要实现的类似于标准库的 thread::spawn,所以我们可以观察
thread::spawn 的签名在其参数中使用了何种 bound。查看文档会发现:
pub fn spawn<F, T>(f: F) -> JoinHandle
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
F 是这里我们关心的参数;T 与返回值有关所以我们并不关心。考虑到 spawn 使用 FnOnce 作为 F 的 trait
bound,这可能也是我们需要的,因为最终会将传递给 execute 的参数传给 spawn。因为处理请求的线
程只会执行闭包一次,这也进一步确认了 FnOnce 是我们需要的 trait,这里符合 FnOnce 中 Once 的意思。
F 还有 trait bound Send 和生命周期绑定 ’ static,这对我们的情况也是有意义的:需要 Send 来将闭包
从一个线程转移到另一个线程,而 ’ static 是因为并不知道线程会执行多久。让我们编写一个使用带有
这些 bound 的泛型参数 F 的 ThreadPool 的 execute 方法:
文件名: src∕lib.rs
pub struct ThreadPool;
impl ThreadPool {
// --snip–
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
pub fn execute(&self, f: F)
542 CHAPTER 20. 最后的项目: 构建多线程 WEB SERVER
where
F: FnOnce() + Send + 'static,
{
}
}
FnOnce trait 仍然需要之后的 (),因为这里的 FnOnce 代表一个没有参数也没有返回值的闭包。正如函
数的定义,返回值类型可以从签名中省略,不过即便没有参数也需要括号。
这里再一次增加了 execute 方法的最小化实现:它没有做任何工作,只是尝试让代码能够编译。再次进
行检查:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
现在就只有警告了!这意味着能够编译了!注意如果尝试 cargo run 运行程序并在浏览器中发起请求,
仍会在浏览器中出现在本章开始时那样的错误。这个库实际上还没有调用传递给 execute 的闭包!
一个你可能听说过的关于像 Haskell 和 Rust 这样有严格编译器的语言的说法是 ” 如果代码能够编
译,它就能工作”。这是一个提醒大家的好时机,实际上这并不是普适的。我们的项目可以编译,不
过它完全没有做任何工作!如果构建一个真实且功能完整的项目,则需花费大量的时间来开始编写
单元测试来检查代码能否编译 并且拥有期望的行为。