如下是我们将怎样构建此 Web 服务器的计划:
TCP
与 HTTP
知识(socket)
上监听 TCP
请求HTTP
请求HTTP
响应server
的吞吐量不过在开始之前,需要提到一点细节:这里使用的方法并不是使用 Rust 构建 Web 服务器最好的方法。crates.io
上有很多可用于生产环境的 crate,它们提供了比我们所要编写的更为完整的 Web
服务器和线程池实现。
然而,本章的目的在于学习,而不是走捷径。因为 Rust 是一个系统编程语言,我们能够选择处理什么层次的抽象,并能够选择比其他语言可能或可用的层次更低的层次。因此我们将自己编写一个基础的 HTTP server
和线程池
,以便学习将来可能用到的 crate 背后的通用理念和技术。
Web
服务器,不过在开始之前,我们将快速了解一下构建 Web
服务器所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供我们所需的信息。std::net
模块处理这些功能。让我们一如既往新建一个项目:cargo new hello
Created binary (application) `hello` project
cd hello
src/main.rs
输入示例中的代码作为开始。这段代码会在地址 127.0.0.1:7878
上监听传入的 TCP 流。当获取到传入的流,它会打印出 Connection established!:
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Connection established!");
}
}
TcpListener
用于监听 TCP
连接。我们选择监听地址 127.0.0.1:7878
。将这个地址拆开,冒号之前的部分是一个代表本机的 IP 地址(这个地址在每台计算机上都相同,并不特指作者的计算机),而 7878
是端口。选择这个端口出于两个原因:通常 HTTP
接受这个端口而且 7878
在电话上打出来就是 "rust"
(译者注:九宫格键盘上的英文)。bind
函数类似于 new
函数,在这里它返回一个新的 TcpListener
实例。这个函数叫做 bind
是因为,在网络领域,连接到监听端口被称为 “绑定到一个端口”(“binding to a port”)bind
函数返回 Result
,这表明绑定可能会失败,例如,连接 80 端口需要管理员权限(非管理员用户只能监听大于 1024 的端口),所以如果不是管理员尝试连接 80 端口,则会绑定失败。另一个例子是如果运行两个此程序的实例这样会有两个程序监听相同的端口,绑定会失败。因为我们是出于学习目的来编写一个基础的服务器,将不用关心处理这类错误,使用 unwrap
在出现这些情况时直接停止程序。TcpListener
的 incoming
方法返回一个迭代器,它提供了一系列的流(更准确的说是 TcpStream
类型的流)。流(stream)代表一个客户端和服务端之间打开的连接。连接(connection)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。为此,TcpStream 允许我们读取它来查看客户端发送了什么,并可以编写响应。总体来说,这个 for
循环会依次处理每个连接并产生一系列的流供我们处理。unwrap
调用,如果出现任何错误会终止程序,如果没有任何错误,则打印出信息。下一个示例我们将为成功的情况增加更多功能。当客户端连接到服务端时 incoming
方法返回错误是可能的,因为我们实际上没有遍历连接,而是遍历 连接尝试(connection attempts)。连接可能会因为很多原因不能成功,大部分是操作系统相关的。例如,很多系统限制同时打开的连接数;新连接尝试产生错误,直到一些打开的连接关闭为止。cargo run
,接着在浏览器中加载 127.0.0.1:7878
。浏览器会显示出看起来类似于**“连接重置”**(“Connection reset”)的错误信息,因为服务端目前并没响应任何数据。但是如果我们观察终端,会发现当浏览器连接服务端时会打印出一系列的信息!tab
标签中的 favicon.ico
。stream
在循环的结尾离开作用域并被丢弃,其连接将被关闭,作为 drop 实现的一部分。浏览器有时通过重连来处理关闭的连接,因为这些问题可能是暂时的。现在重要的是我们成功的处理了 TCP
连接!ctrl-C
来停止程序。并在做出最新的代码修改之后执行 cargo run
重启服务。handle_connection
函数中,我们从 TCP
流中读取数据并打印出来以便观察浏览器发送过来的数据。use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
}
std::io::prelude
引入作用域来获取读写流所需的特定 trait
。在 main
函数的 for
循环中,相比获取到连接时打印信息,现在调用新的 handle_connection
函数并向其传递 stream
。handle_connection
中,stream
参数是可变的。这是因为 TcpStream
实例在内部记录了所返回的数据。它可能读取了多于我们请求的数据并保存它们以备下一次请求数据。因此它需要是 mut
的因为其内部状态可能会改变;通常我们认为 “读取” 不需要可变性,不过在这个例子中则需要 mut
关键字。buffer
来存放读取到的数据。这里创建了一个 1024
字节的缓冲区,它足以存放基本请求的数据并满足本章的目的需要。如果希望处理任意大小的请求,缓冲区管理将更为复杂,不过现在一切从简。接着将缓冲区传递给 stream.read
,它会从 TcpStream
中读取字节并放入缓冲区中。String::from_utf8_lossy
函数获取一个 &[u8]
并产生一个 String。
函数名的 “lossy”
部分来源于当其遇到无效的 UTF-8
序列时的行为:它使用 �,U+FFFD REPLACEMENT CHARACTER,来代替无效序列。你可能会在缓冲区的剩余部分看到这些替代字符,因为他们没有被请求数据填满。$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42 secs
Running `target/debug/hello`
Request: GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101
Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
������������������������������������
Request: GET
之后的路径来解释为何会从浏览器得到多个连接。如果重复的连接都是请求 /
,就知道了浏览器尝试重复获取 / 因为它没有从程序得到响应。Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
method
,比如 GET
或 POST
,这描述了客户端如何进行请求。这里客户端使用了 GET
请求。/
,它代表客户端请求的 统一资源标识符(Uniform Resource Identifier,URI) —— URI
大体上类似,但也不完全类似于 URL
(统一资源定位符,Uniform Resource Locators)。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI,所以这里可以简单的将 URL 理解为 URI。HTTP
版本,然后请求行以 CRLF
序列 (CRLF
代表回车和换行,carriage return line feed,这是打字机时代的术语!)结束。CRLF
序列也可以写成 \r\n
,其中 \r
是回车符,\n
是换行符。 CRLF
序列将请求行与其余请求数据分开。 请注意,打印 CRLF
时,我们会看到一个新行,而不是 \r\n
。GET
是 method
,/
是请求 URI
,而 HTTP/1.1
是版本。Host:
开始的其余的行是 headers;GET 请求没有 body
。127.0.0.1:7878/test
,来观察请求数据如何变化。HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
CRLF
序列之后是任意 header,另一个 CRLF
序列,和响应的 body。HTTP 1.1
版本的响应例子,其状态码为 200
,原因短语为 OK
,没有 header,也没有 body:HTTP/1.1 200 OK\r\n\r\n
handle_connection
函数中,我们需要去掉打印请求数据的 println!
,并替换为示例中的代码:use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
response
来存放将要返回的成功响应的数据。接着,在 response
上调用 as_bytes
,因为 stream
的 write
方法获取一个 &[u8]
并直接将这些字节发送给连接。write
操作可能会失败,所以像之前那样对任何错误结果使用 unwrap
。同理,在真实世界的应用中这里需要添加错误处理。最后,flush
会等待并阻塞程序执行直到所有字节都被写入连接中;TcpStream
包含一个内部缓冲区来最小化对底层操作系统的调用。Cargo
以外的任何输出。不过当在浏览器中加载 127.0.0.1:7878
时,会得到一个空页面而不是错误。太棒了!我们刚刚手写了一个 HTTP 请求与响应。hello.html
—— 而不是在 src
目录中。在此可以放入任何你期望的 HTML
;列表展示了一个可能的文本:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
HTML5
文档,它有一个标题和一小段文本。为了在服务端接受请求时返回它,需要如示例所示修改 handle_connection
来读取 HTML
文件,将其加入到响应的 body 中,并发送:use std::fs;
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let contents = fs::read_to_string("hello.html").unwrap();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
contents.len(),
contents
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
File
引入作用域。打开和读取文件的代码应该看起来很熟悉,因为 I/O 项目的示例中读取文件内容时出现过类似的代码format!
将文件内容加入到将要写入流的成功响应的 body 中。cargo run
运行程序,在浏览器加载 127.0.0.1:7878
,你应该会看到渲染出来的 HTML 文件!buffer
中的请求数据并无条件的发送了 HTML
文件的内容。这意味着如果尝试在浏览器中请求 127.0.0.1:7878/something-else
也会得到同样的 HTML
响应。如此其作用是非常有限的,也不是大部分服务端所做的;让我们检查请求并只对格式良好(well-formed
)的请求 / 发送 HTML 文件。Web
服务器不管客户端请求什么都会返回相同的 HTML
文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 /
,并在其请求任何其他内容时返回错误的功能。为此需要如示例那样修改 handle_connection
。新代码接收到的请求的内容与已知的 /
请求的一部分做比较,并增加了 if
和 else
块来区别处理请求: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";
if buffer.starts_with(get) {
let contents = fs::read_to_string("hello.html").unwrap();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
contents.len(),
contents
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
} else {
// 其他请求
}
}
/
请求相关的数据硬编码进变量 get
。因为我们将原始字节读取进了缓冲区,所以在 get
的数据开头增加 b""
字节字符串语法将其转换为字节字符串。接着检查 buffer
是否以 get
中的字节开头。如果是,这就是一个格式良好的 /
请求,也就是 if
块中期望处理的成功情况,并会返回 HTML
文件内容的代码。buffer
不 以 get
中的字节开头,就说明接收的是其他请求。之后会在 else
块中增加代码来响应所有其他请求。127.0.0.1:7878
,就会得到 hello.html
中的 HTML。如果进行任何其他请求,比如 127.0.0.1:7878/something-else
,则会得到像运行示例中代码那样的连接错误。else
块增加代码返回一个带有404
状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML
向浏览器终端用户表明此意:else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").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();
}
404
和原因短语 NOT FOUND
。仍然没有返回任何 header
,而其 body
将是 404.html
文件中的 HTML
。需要在 hello.html
同级目录创建 404.html
文件作为错误页面;这一次也可以随意使用任何 HTML
或使用示例中的示例 HTML
:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
127.0.0.1:7878
应该会返回 hello.html
的内容,而对于任何其他请求,比如 127.0.0.1:7878/foo
,应该会返回 404.html
中的错误 HTML
!if
和 else
块中的代码有很多的重复:他们都读取文件并将其内容写入流。唯一的区别是状态行和文件名。为了使代码更为简明,将这些区别分别提取到一行 if
和 else
中,对状态行和文件名变量赋值;然后在读取文件和写入响应的代码中无条件的使用这些变量。重构后取代了大段 if
和 else
块代码后的结果如示例所示:// --snip--
fn handle_connection(mut stream: TcpStream) {
// --snip--
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("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();
}
现在 if
和 else
块所做的唯一的事就是在一个元组中返回合适的状态行和文件名的值;接着使用使用模式的 let
语句通过解构元组的两部分为 filename
和 header
赋值。
之前读取文件和写入响应的冗余代码现在位于 if
和 else
块之外,并会使用变量 status_line
和 filename
。这样更易于观察这两种情况真正有何不同,还意味着如果需要改变如何读取文件或写入响应时只需要更新一处的代码。示例中代码的行为与示例完全一样。
好极了!我们有了一个 40 行左右 Rust 代码的小而简单的服务器,它对一个请求返回页面内容而对所有其他请求返回 404 响应。
目前服务器运行于单线程中,它一次只能处理一个请求。让我们模拟一些慢请求来看看这如何会成为一个问题,并进行修复以便服务器可以一次处理多个请求。
/sleep
请求处理,它会使服务器在响应之前休眠五秒。fn main() {
use std::thread;
use std::time::Duration;
use std::io::prelude::*;
use std::net::TcpStream;
use std::fs::File;
// --snip--
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
// --snip--
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\r\n\r\n", "hello.html")
} else if buffer.starts_with(sleep) {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "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();
}
sleep
,我们会识别其数据。在 if
块之后增加了一个 else if
来检查 /sleep
请求,当接收到这个请求时,在渲染成功 HTML
页面之前会先休眠五秒。cargo run
启动 server
,并接着打开两个浏览器窗口:一个请求 http://127.0.0.1:7878/
而另一个请求 http://127.0.0.1:7878/sleep
。如果像之前一样多次请求 /,会发现响应的比较快速。不过如果请求 /sleep
之后在请求 /
,就会看到 /
会等待直到 sleep
休眠完五秒之后才出现。这里有多种办法来改变我们的 Web 服务器使其避免所有请求都排在慢请求之后;我们将要实现的一个便是线程池
Denial of Service, DoS
)攻击;如果程序为每一个接收的请求都新建一个线程,某人向服务器发起千万级的请求时会耗尽服务器的资源并导致所有请求的处理都被终止。N
个请求,其中 N
为线程数。如果每一个线程都在响应慢请求,之后的请求仍然会阻塞队列,不过相比之前增加了能处理的慢请求的数量。N
个请求,其中 N
为线程数。如果每一个线程都在响应慢请求,之后的请求仍然会阻塞队列,不过相比之前增加了能处理的慢请求的数量。Web
服务器吞吐量的方法之一。其他可供探索的方法有 fork/join
模型和单线程异步 I/O
模型。如果你对这个主题感兴趣,则可以阅读更多关于其他解决方案的内容并尝试用 Rust
实现他们;对于一个像 Rust
这样的底层语言,所有这些方法都是可能的。API
代码的结构,接着在这个结构之内实现功能,而不是先实现功能再设计公有 API
。main
的改变,它在 for
循环中为每一个流分配了一个新线程进行处理:use std::thread;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {}
thread::spawn
会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 /sleep
,接着在另两个浏览器标签页中加载 /
,确实会发现 /
请求不必等待 /sleep
结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。API
的代码做出较大的修改。示例展示我们希望用来替换 thread::spawn
的 ThreadPool
结构体的假想接口:use std::thread;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
struct ThreadPool;
impl ThreadPool {
fn new(size: u32) -> ThreadPool { ThreadPool }
fn execute<F>(&self, f: F)
where F: FnOnce() + Send + 'static {}
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {}
ThreadPool::new
来创建一个新的线程池,它有一个可配置的线程数的参数,在这里是四。这样在 for
循环中,pool.execute
有着类似 thread::spawn
的接口,它获取一个线程池运行于每一个流的闭包。pool.execute
需要实现为获取闭包并传递给池中的线程运行。这段代码还不能编译,不过通过尝试编译器会指导我们如何修复它。cargo check
的编译器错误来驱动开发。下面是我们得到的第一个错误:$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve. Use of undeclared type or module `ThreadPool`
--> src\main.rs:10:16
|
10 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^^^^^^ Use of undeclared type or module
`ThreadPool`
error: aborting due to previous error
ThreadPool
类型或模块,所以我们将构建一个。ThreadPool
的实现会与 Web 服务器的特定工作相独立,所以让我们从 hello crate 切换到存放 ThreadPool
实现的新库 crate
。这也意味着可以在任何工作中使用这个单独的线程池库,而不仅仅是处理网络请求。ThreadPool
定义:
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
usize
作为 size
参数的类型,因为我们知道为负的线程数没有意义。我们还知道将使用 4 作为线程集合的元素数量,这也就是使用 usize
类型的原因,如 “整数类型” 部分所讲。$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`
--> src/lib.rs:4:16
|
4 | pub fn new(size: usize) -> ThreadPool {
| ^^^^
|
= note: #[warn(unused_variables)] on by default
= note: to avoid this warning, consider using `_size` instead
error[E0599]: no method named `execute` found for type `hello::ThreadPool` in the current scope
--> src/bin/main.rs:18:14
|
18 | pool.execute(|| {
| ^^^^^^^
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<T>
where
F: FnOnce() -> T + 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
方法:pub struct ThreadPool;
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static
{
}
}
FnOnce
trait 仍然需要之后的 ()
,因为这里的 FnOnce
代表一个没有参数也没有返回值的闭包。正如函数的定义,返回值类型可以从签名中省略,不过即便没有参数也需要括号。execute
方法的最小化实现:它没有做任何工作,只是尝试让代码能够编译。再次进行检查:$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`
--> src/lib.rs:4:16
|
4 | pub fn new(size: usize) -> ThreadPool {
| ^^^^
|
= note: #[warn(unused_variables)] on by default
= note: to avoid this warning, consider using `_size` instead
warning: unused variable: `f`
--> src/lib.rs:8:30
|
8 | pub fn execute<F>(&self, f: F)
| ^
|
= note: to avoid this warning, consider using `_f` instead
FnOnce
trait 仍然需要之后的 ()
,因为这里的 FnOnce
代表一个没有参数也没有返回值的闭包。正如函数的定义,返回值类型可以从签名中省略,不过即便没有参数也需要括号。execute
方法的最小化实现:它没有做任何工作,只是尝试让代码能够编译。再次进行检查:$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`
--> src/lib.rs:4:16
|
4 | pub fn new(size: usize) -> ThreadPool {
| ^^^^
|
= note: #[warn(unused_variables)] on by default
= note: to avoid this warning, consider using `_size` instead
warning: unused variable: `f`
--> src/lib.rs:8:30
|
8 | pub fn execute<F>(&self, f: F)
| ^
|
= note: to avoid this warning, consider using `_f` instead
cargo run
运行程序并在浏览器中发起请求,仍会在浏览器中出现在本章开始时那样的错误。这个库实际上还没有调用传递给 execute
的闭包!一个你可能听说过的关于像
Haskell
和Rust
这样有严格编译器的语言的说法是 “如果代码能够编译,它就能工作”。这是一个提醒大家的好时机,实际上这并不是普适的。我们的项目可以编译,不过它完全没有做任何工作!如果构建一个真实且功能完整的项目,则需花费大量的时间来开始编写单元测试来检查代码能否编译 并且 拥有期望的行为。
new
和 execute
的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 new
作为开始。之前选择使用无符号类型作为 size
参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 u32
值。让我们增加在返回 ThreadPool
实例之前检查 size
是否大于零的代码,并使用 assert!
宏在得到零时 panic
,如示例所示:pub struct ThreadPool;
impl ThreadPool {
/// 创建线程池。
///
/// 线程池中线程的数量。
///
/// # Panics
///
/// `new` 函数在 size 为 0 时会 panic。
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
ThreadPool
}
// --snip--
}
ThreadPool
增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic
的情况,尝试运行 cargo doc --open
并点击 ThreadPool
结构体来查看生成的 new
的文档看起来如何!assert!
宏,也可以让 new
像之前 I/O
项目中示例中 Config::new
那样返回一个 Result
,不过在这里我们选择创建一个没有任何线程的线程池应该是不可恢复的错误。如果你想做的更好,尝试编写一个采用如下签名的 new
版本来感受一下两者的区别:pub fn new(size: usize) -> Result<ThreadPool, PoolCreationError> {
ThreadPool
结构体中。不过如何 “储存” 一个线程?让我们再看看 thread::spawn
的签名:pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static
spawn
返回 JoinHandle
,其中 T
是闭包返回的类型。尝试使用 JoinHandle
来看看会发生什么。在我们的情况中,传递给线程池的闭包会处理连接并不返回任何值,所以 T
将会是单元类型 ()
。ThreadPool
的定义来存放一个 thread::JoinHandle<()>
的 vector
实例,使用 size
容量来初始化,并设置一个 for
循环了来运行创建线程的代码,并返回包含这些线程的 ThreadPool
实例:use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut threads = Vec::with_capacity(size);
for _ in 0..size {
// create some threads and store them in the vector
}
ThreadPool {
threads
}
}
// --snip--
}
std::thread
引入库 crate
的作用域,因为使用了 thread::JoinHandle
作为 ThreadPool
中 vector
元素的类型。ThreadPool
新建一个存放 size
个元素的 vector
。本书还未使用过 with_capacity
,它与 Vec::new
做了同样的工作,不过有一个重要的区别:它为 vector
预先分配空间。因为已经知道了 vector
中需要 size
个元素,预先进行分配比仅仅 Vec::new
要稍微有效率一些,因为 Vec::new
随着插入元素而重新改变大小。cargo check
,会看到一些警告,不过应该可以编译成功。for
循环中留下了一个关于创建线程的注释。如何实际创建线程呢?这是一个难题。标准库提供的创建线程的方法,thread::spawn
,它期望获取一些一旦创建线程就应该执行的代码。然而,我们希望开始线程并使其等待稍后传递的代码。标准库的线程实现并没有包含这么做的方法;我们必须自己实现。ThreadPool
和线程间引入一个新数据类型来管理这种新行为。这个数据结构称为 Worker
:这是一个池实现中的常见概念。想象一下在餐馆厨房工作的员工:员工等待来自客户的订单,他们负责接受这些订单并完成它们。JoinHandle<()>
实例的 vector
,我们会储存 Worker
结构体的实例。每一个 Worker
会储存一个单独的 JoinHandle<()>
实例。接着会在 Worker
上实现一个方法,它会获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会赋予每一个 worker id
,这样就可以在日志和调试中区别线程池中的不同 worker
。ThreadPool
时所需的修改。在通过如下方式设置完 Worker
之后,我们会实现向线程发送闭包的代码:
Worker
结构体存放 id
和 JoinHandle<()>
ThreadPool
存放一个 Worker
实例的 vector
Worker::new
函数,它获取一个 id
数字并返回一个带有 id
和用空闭包分配的线程的 Worker
实例ThreadPool::new
中,使用 for
循环计数生成 id
,使用这个 id
新建 Worker
,并储存进 vector
中use std::thread;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl ThreadPool {
// --snip--
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
}
}
// --snip--
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker {
id,
thread,
}
}
}
ThreadPool
中字段名从threads
改为workers
,因为它现在储存 Worker
而不是 JoinHandle<()>
。使用 for
循环中的计数作为 Worker::new
的参数,并将每一个新建的 Worker
储存在叫做 workers
的 vector
中。Worker
结构体和其 new
函数是私有的,因为外部代码(比如 src/bin/main.rs 中的 server)并不需要知道关于 ThreadPool
中使用 Worker
结构体的实现细节。Worker::new
函数使用 id
参数并储存了使用一个空闭包创建的 JoinHandle<()>
。ThreadPool::new
的参数创建储存了一系列的 Worker
实例,不过 仍然 没有处理 execute
中得到的闭包。让我们聊聊接下来怎么做。thread::spawn
的闭包完全没有做任何工作。目前,我们在 execute
方法中获得期望执行的闭包,不过在创建 ThreadPool
的过程中创建每一个 Worker
时需要向 thread::spawn
传递一个闭包。Worker
结构体能够从 ThreadPool
的队列中获取需要执行的代码,并发送到线程中执行他们。execute
将通过 ThreadPool
向其中线程正在寻找工作的 Worker
实例发送任务。如下是这个计划:
ThreadPool
会创建一个通道并充当发送端Worker
将会充当通道的接收端。Job
结构体来存放用于向通道中发送的闭包。execute
方法会在通道发送端发出期望执行的任务。Worker
会遍历通道的接收端并执行任何接收到的任务。ThreadPool::new
中创建通道并让 ThreadPool
实例充当发送端开始,如示例所示。Job
是将在通道中发出的类型,目前它是一个没有任何内容的结构体:use std::thread;
// --snip--
use std::sync::mpsc;
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
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,
}
}
// --snip--
}
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
参数。示例中展示的代码还不能编译:impl ThreadPool {
// --snip--
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,
}
}
// --snip--
}
// --snip--
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker {
id,
thread,
}
}
}
Worker::new
,并接着在闭包中使用它。check
代码,会得到这个错误:$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:27:42
|
27 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here in
previous iteration of loop
|
= note: move occurs because `receiver` has type
`std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
receiver
传递给多个 Worker
实例。这是不行的,Rust 所提供的通道实现是多 生产者,单 消费者 的。这意味着不能简单的克隆通道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker
中共享单一 receiver
,在线程间分发任务。receiver
,所以这些线程需要一个能安全的共享和修改 receiver
的方式,否则可能导致竞争状态。Arc>
。Arc
使得多个 worker
拥有接收端,而 Mutex
则确保一次只有一个 worker
能从接收端得到任务。示例展示了所需的修改:use std::thread;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
// --snip--
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
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,
}
}
// --snip--
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
let thread = thread::spawn(|| {
receiver;
});
Worker {
id,
thread,
}
}
}
ThreadPool::new
中,将通道的接收端放入一个 Arc
和一个 Mutex
中。对于每一个新 worker
,克隆 Arc
来增加引用计数,如此这些 worker
就可以共享接收端的所有权了。ThreadPool
上的 execute
方法。同时也要修改 Job
结构体:它将不再是结构体,Job
将是一个有着 execute
接收到的闭包类型的 trait
对象的类型别名。类型别名允许将长的类型变短。观察示例:// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
// --snip--
execute
得到的闭包新建 Job
实例之后,将这些任务从通道的发送端发出。这里调用 send
上的 unwrap
,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 unwrap
是因为我们知道失败不可能发生,即便编译器不这么认为。worker
中,传递给 thread::spawn
的闭包仍然还只是 引用 了通道的接收端。相反我们需要闭包一直循环,向通道的接收端请求任务,并在得到任务时执行他们。如示例对 Worker::new
做出修改:// --snip--
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,
}
}
}
receiver
上调用了 lock
来获取互斥器,接着 unwrap
在出现任何错误时 panic
。如果互斥器处于一种叫做 被污染(poisoned)的状态时获取锁可能会失败,这可能发生于其他线程在持有锁时 panic
了且没有释放锁。在这种情况下,调用 unwrap
使其 panic
是正确的行为。请随意将 unwrap
改为包含有意义错误信息的 expect
。recv
从通道中接收 Job
。最后的 unwrap
也绕过了一些错误,这可能发生于持有通道发送端的线程停止的情况,类似于如果接收端关闭时 send
方法如何返回 Err
一样。recv
会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。Mutex
确保一次只有一个 Worker
线程尝试请求任务。cargo run
并发起一些请求:$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field is never used: `workers`
--> src/lib.rs:7:5
|
7 | workers: Vec<Worker>,
| ^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `id`
--> src/lib.rs:61:5
|
61 | id: usize,
| ^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `thread`
--> src/lib.rs:62:5
|
62 | thread: thread::JoinHandle<()>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 0.99 secs
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
/sleep
,server
也能够通过另外一个线程处理其他请求。注意如果同时在多个浏览器窗口打开 /sleep,它们可能会彼此间隔地加载 5 秒,因为一些浏览器处于缓存的原因会顺序执行相同请求的多个实例。这些限制并不是由于我们的 Web 服务器造成的。
use Web_JoinHandle::ThreadPool;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::fs;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
// --snip--
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("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();
}
use std::thread;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// --snip--
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<()>,
}
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!("Wroker {} got a job; executing.",id);
job();
}
});
Worker {
id,
thread,
}
}
}
while let
循环,你可能会好奇为何不能如此编写 worker 线程,如示例所示:// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
while let Ok(job) = receiver.lock().unwrap().recv() {
println!("Worker {} got a job; executing.", id);
job();
}
});
Worker {
id,
thread,
}
}
}
Mutex
结构体没有公有 unlock
方法,因为锁的所有权依赖 lock
方法返回的 LockResult>
中 MutexGuard
的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 Mutex
守护的资源,不过如果没有认真的思考 MutexGuard
的生命周期的话,也可能会导致比预期更久的持有锁。因为 while
表达式中的值在整个块一直处于作用域中,job()
调用的过程中其仍然持有锁,这意味着其他 worker
不能接收任务。loop
并在循环块之内而不是之外获取锁和任务,lock
方法返回的 MutexGuard
在 let job
语句结束之后立刻就被丢弃了。这确保了 recv
调用过程中持有锁,而在 job()
(因为JoinHandle使用的是()返回值)调用前锁就被释放了,这就允许并发处理多个请求了。workers
、id
和 thread
字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 ctrl-c
终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。ThreadPool
实现 Drop trait
对线程池中的每一个线程调用 join
,这样这些线程将会执行完他们的请求。接着会为 ThreadPool
实现一个告诉线程他们应该停止接收新请求并结束的方式。为了实践这些代码,修改 server
在优雅停机(graceful shutdown
)之前只接受两个请求。Drop
。当线程池被丢弃时,应该 join
所有线程以确保他们完成其操作。示例展示了 Drop
实现的第一次尝试;这些代码还不能够编译:impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
workers
。这里使用了 &mut
因为 self
本身是一个可变引用而且也需要能够修改 worker
。对于每一个线程,会打印出说明信息表明此特定 worker
正在关闭,接着在 worker
线程上调用 join
。如果 join
调用失败,通过 unwrap
使得 panic
并进行不优雅的关闭。error[E0507]: cannot move out of borrowed content
--> src/lib.rs:65:13
|
65 | worker.thread.join().unwrap();
| ^^^^^^ cannot move out of borrowed content
join
,因为只有每一个 worker
的可变借用,而 join
获取其参数的所有权。为了解决这个问题,需要一个方法将 thread
移动出拥有其所有权的 Worker
实例以便 join
可以消费这个线程。我们曾见过这么做的方法:如果 Worker
存放的是 Option
,就可以在 Option
上调用 take
方法将值从 Some
成员中移动出来而对 None
成员不做处理。换句话说,正在运行的 Worker
的 thread
将是 Some
成员值,而当需要清理 worker
时,将 Some
替换为 None
,这样 worker
就没有可以运行的线程了。Worker
的定义如下:struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
check
代码会得到两个错误:error[E0599]: no method named `join` found for type
`std::option::Option<std::thread::JoinHandle<()>>` in the current scope
--> src/lib.rs:65:27
|
65 | worker.thread.join().unwrap();
| ^^^^
error[E0308]: mismatched types
--> src/lib.rs:89:13
|
89 | thread,
| ^^^^^^
| |
| expected enum `std::option::Option`, found struct
`std::thread::JoinHandle`
| help: try using a variant of the expected type: `Some(thread)`
|
= note: expected type `std::option::Option<std::thread::JoinHandle<()>>`
found type `std::thread::JoinHandle<_>`
Worker::new
结尾的代码;当新建 Worker
时需要将 thread
值封装进 Some
。做出如下改变以修复问题:impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
Worker {
id,
thread: Some(thread),
}
}
}
Drop
实现中。之前提到过要调用 Option
上的 take
将 thread
移动出 worker
。如下改变会修复问题:impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
Option
上的 take
方法会取出 Some
而留下 None
。使用 if let
解构 Some
并得到线程,接着在线程上调用 join
。如果 worker
的线程已然是 None
,就知道此时这个 worker
已经清理了其线程所以无需做任何操作。Worker
中分配的线程所运行的闭包中的逻辑:调用 join
并不会关闭线程,因为他们一直 loop
来寻找任务。如果采用这个实现来尝试丢弃 ThreadPool
,则主线程会永远阻塞在等待第一个线程结束上。Job
运行也要监听一个应该停止监听并退出无限循环的信号。所以通道将发送这个枚举的两个成员之一而不是 Job
实例:enum Message {
NewJob(Job),
Terminate,
}
Message
枚举要么是存放了线程需要运行的 Job
的 NewJob
成员,要么是会导致线程退出循环并终止的 Terminate
成员。Message
类型值而不是 Job
,如示例 所示:pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Message>,
}
// --snip--
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static
{
let job = Box::new(f);
self.sender.send(Message::NewJob(job)).unwrap();
}
}
// --snip--
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 {} got a job; executing.", id);
job();
},
Message::Terminate => {
println!("Worker {} was told to terminate.", id);
break;
},
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
Message
枚举需要将两个地方的 Job
修改为 Message:ThreadPool
的定义和 Worker::new
的签名。ThreadPool
的 execute
方法需要发送封装进 Message::NewJob
成员的任务。然后,在 Worker::new
中当从通道接收 Message
时,当获取到 NewJob
成员会处理任务而收到 Terminate
成员则会退出循环。Terminate
成员的消息。如示例所示修改 Drop
实现来修复此问题:impl Drop for ThreadPool {
fn drop(&mut self) {
println!("Sending terminate message to all workers.");
for _ in &mut self.workers {
self.sender.send(Message::Terminate).unwrap();
}
println!("Shutting down all workers.");
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
worker
两次,一次向每个 worker
发送一个 Terminate
消息,一个调用每个 worker
线程上的 join
。如果尝试在同一循环中发送消息并立即 join
线程,则无法保证当前迭代的 worker
是从通道收到终止消息的 worker
。worker
的场景。如果在一个单独的循环中遍历每个 worker
,在第一次迭代中向通道发出终止消息并对第一个 worker
线程调用 join
。如果此时第一个 worker
正忙于处理请求,那么第二个 worker
会收到终止消息并停止。我们会一直等待第一个 worker
结束,不过它永远也不会结束因为第二个线程接收了终止消息。死锁!Terminate
消息,接着在另一个循环中 join
所有的线程。每个 worker
一旦收到终止消息即会停止从通道接收消息,意味着可以确保如果发送同 worker
数相同的终止消息,在 join
之前每个线程都会收到一个终止消息。main
在优雅停机 server
之前只接受两个请求:fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
web server
只处理两次请求就停机了,这只是为了展示优雅停机和清理处于正常工作状态。take
方法定义于 Iterator trait
,这里限制循环最多头 2 次。ThreadPool
会在 main
的结尾离开作用域,而且还会看到 drop
实现的运行。cargo run
启动 server
,并发起三个请求。第三个请求应该会失败,而终端的输出应该看起来像这样:$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 1.0 secs
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 3 got a job; executing.
Shutting down.
Sending terminate message to all workers.
Shutting down all workers.
Shutting down worker 0
Worker 1 was told to terminate.
Worker 2 was told to terminate.
Worker 0 was told to terminate.
Worker 3 was told to terminate.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
worker
和信息输出。可以从信息中看到服务是如何运行的: worker 0
和 worker 3
获取了头两个请求,接着在第三个请求时,我们停止接收连接。当 ThreadPool
在 main
的结尾离开作用域时,其 Drop
实现开始工作,线程池通知所有线程终止。每个 worker
在收到终止消息时会打印出一个信息,接着线程池调用 join
来终止每一个 worker
线程。join worker 0
了。worker 0
还没有收到终止消息,所以主线程阻塞直到 worker 0
结束。与此同时,每一个线程都收到了终止消息。一旦 worker 0
结束,主线程就等待其他 worker
结束,此时他们都已经收到终止消息并能够停止了。web server
。我们能对 server
执行优雅停机,它会清理线程池中的所有线程。