mirror of
https://github.com/gnu4cn/rust-lang-zh_CN.git
synced 2026-08-19 04:33:27 +08:00
Updated 'src/final_project/mutlithreaded.md'.
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufReader, prelude::*},
|
||||
io::{prelude::*, BufReader},
|
||||
net::{TcpListener, TcpStream},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
handle_connection(stream);
|
||||
pool.execute(|| {
|
||||
handle_connection(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +23,16 @@ fn handle_connection(mut stream: TcpStream) {
|
||||
let buf_reader = BufReader::new(&stream);
|
||||
let request_line = buf_reader.lines().next().unwrap().unwrap();
|
||||
|
||||
let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
|
||||
("HTTP/1.1 200 OK", "hello.html")
|
||||
} else {
|
||||
("HTTP/1.1 404 NOT FOUND", "404.html")
|
||||
let (status_line, filename) = match &request_line[..] {
|
||||
"GET / HTTP/1.1" => ( "HTTP/1.1 200 OK", "hello.html"),
|
||||
"GET /sleep HTTP/1.1" => {
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
("HTTP/1.1 200 0K", "hello.html")
|
||||
}
|
||||
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
|
||||
};
|
||||
|
||||
|
||||
let contents = fs::read_to_string(filename).unwrap();
|
||||
let length = contents.len();
|
||||
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
# 从单线程服务器到多线程服务器
|
||||
|
||||
现在,这个服务器将依次处理每个请求,这意味着其将不会在前一个连接完成处理前,处理后一连接。若服务器收到了越来越多的请求,这种顺序执行就会越来越差。而若该服务器收到了一个要耗费较长时间处理的请求,即使后续的新请求可被快速处理,但其仍将不得不等待直到那个长时间请求完成。咱们需要修复这个问题,但首选,咱们将具体看看这个问题。
|
||||
目前,服务器将依次处理每个请求,这意味着在第一个连接处理完毕之前,他不会处理第二个连接。当服务器收到越来越多的请求时,这种串行执行将越来越不理想。当服务器收到一个需要很长时间处理的请求时,后续请求就必须等该该请求处理完毕,即使这些新请求可被快速处理。我们需要解决这个问题,但首选我们将看看实际操作中的问题。
|
||||
|
||||
|
||||
## 在当前服务器实现下模拟一个慢速请求
|
||||
## 模拟慢速请求
|
||||
|
||||
**Simulating a Slow Request in the Current Server Implemenation**
|
||||
我们将探讨处理慢速请求会怎样影响向当前服务器发出的其他请求。下面清单 21-10 实现了通过模拟慢速响应来处理对 `/sleep` 的请求,这将导致服务器在响应之前休眠 5 秒钟。
|
||||
|
||||
|
||||
咱们将看看一个慢速处理的请求,能怎样影响那些到咱们当前服务器实现的其他请求。下面清单 20-10 以一个将导致服务器在响应前睡眠 5 秒的模拟慢速请求,实现了对到 `/sleep` 请求的处理。
|
||||
|
||||
文件名:`src/main.rs`
|
||||
<a name="listing_21-10"></a>
|
||||
文件名:`projects/hello/src/main.rs`
|
||||
|
||||
```rust
|
||||
#![allow(warnings)]
|
||||
use std::{
|
||||
fs,
|
||||
io::{prelude::*, BufReader},
|
||||
net::{TcpListener, TcpStream},
|
||||
thred,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
// --跳过代码--
|
||||
|
||||
fn handle_conn(mut stream: TcpStream) {
|
||||
fn handle_connection(mut stream: TcpStream) {
|
||||
// --跳过代码--
|
||||
|
||||
let (status_line, filename) = match &req_line[..] {
|
||||
let (status_line, filename) = match &request_line[..] {
|
||||
"GET / HTTP/1.1" => ( "HTTP/1.1 200 OK", "hello.html"),
|
||||
"GET /sleep HTTP/1.1" => {
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
@@ -39,44 +36,48 @@ fn handle_conn(mut stream: TcpStream) {
|
||||
}
|
||||
```
|
||||
|
||||
*清单 20-10:通过睡眠 5 秒模拟慢速请求*
|
||||
**清单 21-10**:通过休眠 5 秒来模拟慢速请求
|
||||
|
||||
现在咱们有了三种情况,于是就已从 `if` 切换到了 `match`。咱们需要显式地在 `req_line` 切片上,与那三个字符串字面值进行模式匹配;`match` 不会像相等比较方式所做的那样,执行自动引用与解引用。
|
||||
现在我们有三种情况,于是已从 `if` 切换为 `match`。我们需要显式地对 `request_line` 的一个切片匹配,以与字符串字面值模式匹配;`match` 不会像相等比较方式那样,执行自动引用和解引用。
|
||||
|
||||
首条支臂与清单 20-9 的 `if` 代码块是一样的。第二条支臂,是将请求与 `/sleep` 匹配。在收到那个请求时,服务器将在渲染那个成功 HTML 页面之前,睡眠 5 秒。第三支臂则与清单 20-9 的那个 `else` 代码块是一样的。
|
||||
第一个支臂与 [清单 21-9](./single-threaded.md#listing_21-9) 中的 `if` 代码块相同。第二个支臂匹配到 `/sleep` 的请求。收到该请求后,服务器将在渲染成功 HTML 页面之前休眠 5 秒。第三个支臂与清单 21-9 中的 `else` 代码块相同。
|
||||
|
||||
咱们可以看出,咱们的服务器有多原始:真正的库将以一种不那么冗长的方式,处理多种请求的识别!
|
||||
咱们可以看到我们的服务器是多么的原始:真正的库将以更简洁的方式处理多个请求的识别!
|
||||
|
||||
请使用 `cargo run` 启动服务器。随后打开两个浏览器窗口:一个用于 `http://127.0.0.1/7878`,另一个用于 `http://127.0.0.1:7878/sleep`。若咱们像之前一样进入那个 `/` URI 几次,咱们将看到其响应很快。但在进入 `/sleep` 并于随后加载 `/` 时,就会看到那个 `/` 会一直等待,直到 `sleep` 已经于加载之前睡眠了 5 秒。
|
||||
请使用 `cargo run` 启动服务器。然后,打开两个浏览器窗口:一个用于 `http://127.0.0.1/7878`,另一个用于 `http://127.0.0.1:7878/sleep`。当咱们像之前那样多次输入 `/` 的 URI,咱们将发现他响应很快。但当咱们先输入 `/sleep`,然后加载 `/` 时,就会发现 `/` 会等待 `sleep` 休眠 5 秒后才加载。
|
||||
|
||||
咱们可以用来避免慢速请求后面那些请求滞后的技巧有多种;咱们将实现的技巧,便是线程池。
|
||||
我们可使用多种技术,来避免请求在慢速请求后面积压,包括像在第 17 中那样使用异步;我们将实现的是线程池。
|
||||
|
||||
|
||||
## 使用线程池提升吞吐量
|
||||
## 通过线程池提升吞吐量
|
||||
|
||||
**Improving Throughput with a Thread Pool**
|
||||
所谓 *线程池,thread pool*,是一组已创建的线程,他们出于就绪状态并等待处理任务。当程序接收到新任务时,他会将池中的线程之一分配给该任务,进而该线程将处理该任务。在第一个线程处理期间,池中剩余的线程可用于处理任何新到任务。当第一个线程处理完其任务后,他会被返回到空闲线程池,准备处理新任务。线程池允许咱们同时处理连接,从而提高服务器的吞吐量。
|
||||
|
||||
我们将把线程池中的线程数量限制为少量,以保护我们免受拒绝服务,DoS,攻击;若我们让程序为每个传入的请求都创建一个新线程,那么当某人向我们的服务器发出 1000 万次请求时,就会耗尽服务器的所有资源,导致请求处理彻底瘫痪,从而造成严重破坏。
|
||||
|
||||
所谓 *线程池,thread pool*,是指处于等待中,并准备好处理某项任务的一组生成的线程。在程序收到一项新任务时,他便指派线程池中的一个线程给该项任务,而那个线程就会处理这个任务。池中的剩余线程,则是可以处理任何的于这首个线程进行处理时,进来的那些任务的。在这首个线程完成其任务处理时,他就会回到空闲线程的线程池,准备处理某项新任务。线程池实现了连接的并发处理,从而提升咱们服务器的吞吐能力。
|
||||
因此,与其生成无限数量的线程,我们不如让固定数量的线程在池中等待。传入的请求将发送到池中进行处理。线程池将维护一个传入请求的队列。池中的每个线程都将弹出池中的一个请求,处理该请求,然后向队列请求另一个请求。在这种设计下,我们最多可以同时处理 `N` 个请求,其中 `N` 是线程数。当每个线程都在处理耗时较长的请求时,后续请求仍然会在队列中积压,但我们增加了在到达该临界点之前,可以处理的耗时请求的数量。
|
||||
|
||||
咱们将把池中线程数量,先知道一个较小的数目,以保护咱们免于拒绝服务攻击,Denial of Service(DoS) attacks;若咱们让咱们的程序在每个请求进入时,创建一个新线程,那么构造出一千万个请求到咱们的服务器的某人,就能经由耗尽咱们服务器的全部资源,而使得这些请求的处理陷入停滞,而造成极大破坏。
|
||||
这种技术只是提高 web 服务器吞吐量的众多方法之一。咱们可能探索的其他选项,比如
|
||||
|
||||
这种技巧只是提供 web 服务器吞吐量的许多方法之一。咱们可能探讨的其他选项分别是 *分叉汇合模型,fork/join model*、*单线程异步 I/O 模型,single-threaded async I/O model*,抑或 *多线程异步 I/O 模型,multi-threaded async I/O model*。若对此问题感兴趣,那么可以阅读有关其他解决方案的资料,并尝试实现他们;对于 Rust 这种底层编程语言,所有这些选项都是可行的。
|
||||
- 分叉汇合模型,fork/join model、
|
||||
- 单线程异步 I/O 模型,single-threaded async I/O model,
|
||||
- 以及多线程异步 I/O 模型,multi-threaded async I/O model 等等。
|
||||
|
||||
若咱们对这一主题感兴趣,可以进一步了解其他解决方案并尝试实现他们;对于 Rust 这样的底层编程语言,所有这些选项都是可行的。
|
||||
|
||||
在开始实现线程池前,咱们来聊聊用到这个池子的东西会是什么样子。在咱们正要尝试设计代码时,首先编写客户端界面,可有助于引导咱们的设计。要以咱们打算调用代码 API 的方式,编写出这些有组织架构的代码 API;随后在那种组织架构下实现功能,而非先实现功能而随后设计那些公开 API。
|
||||
在开始实现线程池之前,我们来先讨论以下使用线程池子应呈现何种形态。当咱们尝试设计代码时,首先编写客户端接口有助于引导咱们的设计思路。应按照咱们希望调用代码的组织方式编写代码的 API;然后,在这种组织方式下实现功能,而不是先实现功能再设计公开 API。
|
||||
|
||||
与第 12 章中项目里用到的测试驱动方式的开发,test-driven development,类似,这里咱们将运用编译器驱动的开发,compiler-driven development。咱们将先编写出咱们打算调用那些函数的代码,而随后会看看来自编译器的那些报错,以确定出接下来咱们应修改些什么,来让代码运作起来。在咱们进行那一步之前,咱们将探讨一下咱们并不会用到的一种技巧,作为开头。
|
||||
与我们在第 12 章中的项目中使用的测试驱动开发的方式类似,我们在这里将使用编译器驱动开发,compiler-driven development。我们将编写所需函数的代码,然后我们将查看编译器中的报错,以确定下一步应如何修改代码使其正常运行。但在开始之前,我们将先探讨一种我们不会使用的技术作为起点。
|
||||
|
||||
|
||||
### 为每个请求生成一个线程
|
||||
|
||||
**Spawning a Thread for Each Request**
|
||||
首先,我们来探讨一下,当为每个连接都创建一个新线程时,我们的代码会是什么样子。正如早先提到的,由于可能生成无限数量的线程,这并非我们的最终方案,但他是构建一个可运行的多线程服务器的起点。然后,我们将添加线程池作为改进,从而对比这两种方案会更容易。
|
||||
|
||||
下面清单 21-11 展示了对 `main` 构造的更改,以便在 `for` 循环内为处理每个流而生成新线程。
|
||||
|
||||
首先,咱们来探讨一下若咱们的代码给每隔连接创建一个新线程,他看起来会怎样。正如早先所提到的,由于潜在地生成无限数目线程的那些问题,这样做不是咱们的最终计划,但其为首先得到一个运作多线程服务器的起点。随后咱们将添加线程池作为一项改进,且将这两种方案进行对比将更容易一些。下面清单 20-11 给出了把 `main` 构造为于那个 `for` 循环里,生成一个新线程来处理每个 TCP 流的一些修改。
|
||||
|
||||
文件名:`src/main.rs`
|
||||
<a name="listing_21-11"></a>
|
||||
文件名:`projects/hello/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
@@ -86,24 +87,24 @@ fn main() {
|
||||
let stream = stream.unwrap();
|
||||
|
||||
thread::spawn(|| {
|
||||
handle_conn(stream);
|
||||
handle_connection(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*清单 20-11:为每个 TCP 流生成一个新线程*
|
||||
**清单 21-11**:为每个流都生成一个新线程
|
||||
|
||||
如同咱们在第 16 章中所学到的,`thread::spawn` 讲创建出一个新线程,并于随后在新线程中,运行那个闭包中的代码。当咱们运行此代码,并在浏览器中加载 `/sleep`,随后在另外两个浏览器 Tab 页中加载 `/`,咱们就会看到到 `/` 的请求就不必等待 `/sleep` 请求完毕了。不过,如同咱们曾提到过的,因为咱们正不带任何限制地构造新线程,而最终将使系统不堪重负。
|
||||
正如咱们在第 16 章中所学到的,`thread::spawn` 将创建一个新线程,然后在新线程中运行闭包中的代码。当咱们运行这段代码,并在浏览器中加载 `/sleep`,然后在另外两个浏览器 Tab 页中加载 `/`,咱们确实会发现到 `/` 的请求不必等待 `/sleep` 完成。然而,正如我们提到的,这最终将使系统不堪重负,因为咱们会无限制地创建新线程。
|
||||
|
||||
咱们可能还记得第 17 章中的内容,这正是异步和等待真正大显身手的情形!在我们构建线程池时请记住这一点,并思考在异步下会有何不同或相同点。
|
||||
|
||||
|
||||
### 创建有限数目的线程
|
||||
|
||||
**Creating a Finite Number of Threads**
|
||||
|
||||
|
||||
咱们想要咱们的线程池,以类似的、熟悉的方式运作,而无需那些用到咱们 API 的代码有较大修改。下面清单 20-12 给出了咱们打算用到的 `ThreadPool`,而非 `thread::spawn`,的假想接口。
|
||||
我们希望线程池以类似、熟悉的方式工作,这样在使用我们 API 的代码中,从单线程切换到线程池是,就无需进行大量更改。下面清单 21-12 展示了我们打算用来替换 `thread::spawn` 的 `ThreadPool` 结构体的假设接口。
|
||||
|
||||
<a name="listing_21-12"></a>
|
||||
文件名:`src/main.rs`
|
||||
|
||||
```rust
|
||||
@@ -115,38 +116,35 @@ fn main() {
|
||||
let stream = stream.unwrap();
|
||||
|
||||
pool.execute(|| {
|
||||
handle_conn(stream);
|
||||
handle_connection(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*清单 20-12:咱们设想的 `ThreadPool` 接口*
|
||||
**清单 21-12**:我们的理想 `ThreadPool` 接口
|
||||
|
||||
咱们使用了 `ThreadPool::new` 来创建出有着可配置线程数目的新线程,在此示例中为四个线程。随后,在那个 `for` 循环中,`pool.execute` 有着与 `thread::spawn` 类似的接口,其中他会取个闭包,并将其给到线程池中的某个线程运行。这段代码尚不会编译,但咱们将进行尝试,如此编译器就会引导咱们如何修复他。
|
||||
我们使用 `ThreadPool::new` 创建一个线程池,带有可配置的线程数量,在这一情形下为四个。然后,在 `for` 循环中,`pool.execute` 有着与 `thread::spawn` 类似的接口,即他取一个闭包,线程池应针对每个流运行该闭包。我们需要实现 `pool.execute` 方法,使其取闭包并将该闭包交由线程池中的某个线程执行。这段代码还不会编译,但我们将尝试编译,以便编译器可以指导我们如何修复他。
|
||||
|
||||
|
||||
### 运用编译器驱动的开发,构建出 `ThreadPool`
|
||||
### 使用编译器驱动开发构建 `ThreadPool`
|
||||
|
||||
**Building `ThreadPool` Using Compiler Driven Development**
|
||||
|
||||
|
||||
请完成清单 20-12 中对 `src/main.rs` 的修改,然后咱们就来运用 `cargo check` 给出的编译器报错,驱动咱们的开发。下面就是咱们所得到的第一个报错:
|
||||
请对 `src/main.rs` 进行清单 21-12 中的修改,然后我们来运用 `cargo check` 中的编译器报错驱动我们的开发。下面是我们得到的第一个报错:
|
||||
|
||||
```console
|
||||
$ cargo check
|
||||
Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello)
|
||||
Checking hello v0.1.0 (/home/hector/rust-lang-zh_CN/projects/hello)
|
||||
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
|
||||
--> src/main.rs:12:16
|
||||
--> src/main.rs:11:16
|
||||
|
|
||||
12 | let pool = ThreadPool::new(4);
|
||||
11 | 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
|
||||
error: could not compile `hello` (bin "hello") due to 1 previous error
|
||||
```
|
||||
|
||||
很棒!这个错误告诉我们,咱们需要一个 `ThreadPool` 类型或模组,因此咱们现在就将构建一个出来。咱们的 `ThreadPool` 实现,将独立于咱们的 web 服务器所完成工作的类型。因此,咱们就来将这个 `hello` 代码箱,从二进制代码箱切换为一个库代码箱,来保存咱们的 `ThreadPool` 实现。在咱们改变为库代码箱后,咱们就可以在打算用到线程池的任何项目,而不只是用来服务 web 请求中,也可以使用这个独立的线程池了。
|
||||
太好了!这个错误告诉我们,我们需要一个 `ThreadPool` 类型或模组,所以我们现在就构建一个。我们的 `ThreadPool` 实现将独立于我们的 web 服务器正在执行的工作类别。因此,咱们就来将这个 `hello` 代码箱,从二进制代码箱切换为一个库代码箱,来保存咱们的 `ThreadPool` 实现。在咱们改变为库代码箱后,咱们就可以在打算用到线程池的任何项目,而不只是用来服务 web 请求中,也可以使用这个独立的线程池了。
|
||||
|
||||
请创建一个包含了下面这个咱们目前所能有的 `ThreadPool` 结构体极简定义的 `src/lib.rs` 文件:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user