Chapter 21
Final Project: Building a Multithreaded Web Server
It’s been a long journey, but we’ve reached the end of the book. One last project, demonstrating concepts from the final chapters and recapping earlier lessons: a web server that says “Hello!” in a browser. The plan:
- Learn a bit about TCP and HTTP.
- Listen for TCP connections on a socket.
- Parse a small number of HTTP requests.
- Create a proper HTTP response.
- Improve the throughput of our server with a thread pool.
- 21.1 Building a Single-Threaded Web Server
- 21.2 From a Single-Threaded to a Multithreaded Server
- 21.3 Graceful Shutdown and Cleanup
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
1
Chapter 21 · Final Project
The goal, and two details before we start
Example 21.1. Figure 21-1, our final shared project: a web browser visiting 127.0.0.1:8080 shows a page with the heading “Hello!” and the text “Hi from Rust”.
- The method we’ll use won’t be the best way to build a web server with Rust. Community members have published production-ready crates on crates.io with more complete web server and thread pool implementations. Our intention is to help you learn, not to take the easy route. Because Rust is a systems programming language, we can choose the level of abstraction we work with, and go lower than is possible or practical in other languages.
- We will not use async and await. Building a thread pool is a big enough challenge on its own, without also building an async runtime! We will, however, note how async and await might apply to some of the same problems. As noted in Chapter 17, many async runtimes use thread pools for managing their work.
So we write the basic HTTP server and thread pool manually, to learn the general ideas and techniques behind the crates you might use in the future.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
2
Chapter 21 · 21.1 Building a Single-Threaded Web Server
The protocols: HTTP over TCP
We start by getting a single-threaded web server working. The details of the protocols involved are beyond the scope of this book, but a brief overview gives the information we need.
Definition 21.2 (Request-response protocol). A protocol in which a client initiates requests and a server listens to the requests and provides a response to the client. The protocol defines the contents of those requests and responses.
The two main protocols involved in web servers are Hypertext Transfer Protocol (HTTP) and Transmission Control Protocol (TCP). Both are request-response protocols.
- TCP is the lower-level protocol: it describes the details of how information gets from one server to another, but doesn’t specify what that information is.
- HTTP builds on top of TCP by defining the contents of the requests and responses. It’s technically possible to use HTTP with other protocols, but in the vast majority of cases HTTP sends its data over TCP.
We’ll work with the raw bytes of TCP and HTTP requests and responses.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
3
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Listening to the TCP connection
The server first needs to listen to a TCP connection; the standard library’s std::net module lets us do this. Make a new project in the usual fashion. Listing 21.1 listens at 127.0.0.1:7878 for incoming TCP streams and prints Connection established! for each.
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
Listing 21.1: Listening for incoming streams and printing a message when we receive a stream — src/main.rs
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!");
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
4
Chapter 21 · 21.1 Building a Single-Threaded Web Server
TcpListener, the address, and bind
Using TcpListener, we listen for TCP connections at the address 127.0.0.1:7878.
- The section before the colon is an IP address representing your computer. It is the same on every computer and doesn’t represent the authors’ computer specifically.
- 7878 is the port, chosen for two reasons: HTTP isn’t normally accepted on this port, so our server is unlikely to conflict with any other web server running on your machine; and 7878 is rust typed on a telephone.
Definition 21.3 (Binding to a port). In networking, connecting to a port to listen to it. Hence the name of the bind function, which works like a new function: it returns a new TcpListener instance.
bind returns a Result<T, E>: binding can fail — for example, if we ran two instances of our program and so had two programs listening to the same port. Because this is a basic server just for learning, we won’t handle these kinds of errors; we use unwrap to stop the program if errors happen.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
5
Chapter 21 · 21.1 Building a Single-Threaded Web Server
incoming: streams and connection attempts
The incoming method on TcpListener returns an iterator that gives us a sequence of streams — more specifically, streams of type TcpStream.
Definition 21.4 (Stream). An open connection between the client and the server.
Definition 21.5 (Connection). The full request and response process: a client connects to the server, the server generates a response, and the server closes the connection.
So we will read from the TcpStream to see what the client sent, then write our response to the stream to send data back. The for loop processes each connection in turn and produces a series of streams for us to handle.
For now we unwrap each stream (terminating on errors) and print a message. incoming can give errors because we iterate over connection attempts, not connections; they can fail for many, often OS-specific, reasons. E.g. many OSes limit simultaneous open connections, and attempts beyond the limit error until some are closed.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
6
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Running it: several connections per visit
Invoke cargo run, then load 127.0.0.1:7878 in a web browser. The browser shows an error like “Connection reset”, because the server isn’t sending back any data yet. But the terminal shows several messages printed when the browser connected:
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
Why several messages for one browser request?
- The browser may request the page as well as other resources, like the favicon.ico icon that appears in the browser tab.
- The browser may retry, because the server isn’t responding with data. When stream goes out of scope and is dropped at the end of the loop, the connection is closed as part of the drop implementation; browsers sometimes deal with closed connections by retrying, because the problem might be temporary.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
7
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Pre-opened connections; then reading the request
Browsers also sometimes open multiple connections to the server without sending any requests, so that if they do later send requests, those happen more quickly. Our server sees each such connection, whether or not a request is sent over it. Many versions of Chrome-based browsers do this; you can disable that optimization with private browsing mode or by using a different browser.
The important factor is that we’ve successfully gotten a handle to a TCP connection!
Remark. Stop the program with ctrl-C when you’re done running a particular version of the code. Then restart it with cargo run after each set of code changes, to make sure you’re running the newest code.
Reading the request
To separate the concerns of first getting a connection and then taking some action with it, we start a new function for processing connections. The new handle_connection function reads data from the TCP stream and prints it, so we can see the data the browser sends (Listing 21.2).
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
8
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Listing 21.2: main now calls handle_connection
Change the code to look like this:
Listing 21.2: Reading from the TcpStream and printing the data — src/main.rs
use std::{
io::{BufReader, prelude::*},
net::{TcpListener, 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);
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
9
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Listing 21.2: the handle_connection function
Listing 21.2 (continued): Reading from the TcpStream and printing the data — src/main.rs
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
println!("Request: {http_request:#?}");
}
- We bring std::io::BufReader and std::io::prelude into scope to get traits and types that let us read from and write to the stream; main’s loop now passes each stream to handle_connection.
- handle_connection creates a new BufReader that wraps a reference to the stream. BufReader adds buffering by managing calls to the std::io::Read trait methods for us.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
10
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Collecting the request lines
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
- The variable http_request collects the lines of the request the browser sends. The Vec<_> type annotation says we want to collect them in a vector.
- BufReader implements the std::io::BufRead trait, whose lines method returns an iterator of Result<String, std::io::Error>, splitting the data at each newline byte.
- To get each String, we map and unwrap each Result. It might be an error if the data isn’t valid UTF-8 or there was a problem reading from the stream. A production program should handle these errors more gracefully; we stop the program for simplicity.
- The browser signals the end of an HTTP request by sending two newline characters in a row. So to get one request, we take lines until we get a line that is the empty string, then print the vector with pretty debug formatting to look at the instructions the browser sends.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
11
Chapter 21 · 21.1 Building a Single-Threaded Web Server
What the browser sends
Start the program and make a request in a web browser again. The browser still shows an error page, but the terminal output now looks similar to this:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
12
Chapter 21 · 21.1 Building a Single-Threaded Web Server
What the browser sends (continued)
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
Depending on your browser, the output might differ slightly. Now that we print the request data, we can see why one browser request gives multiple connections: look at the path after GET in the first line of the request. If the repeated connections all request /, the browser is trying to fetch / repeatedly because it’s not getting a response from our program.
Next, let’s break down this request data to understand what the browser is asking of our program.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
13
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Looking more closely at an HTTP request
HTTP is a text-based protocol, and a request takes this format:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
Definition 21.6 (Request line). The first line of an HTTP request, holding information about what the client is requesting: method, request URI and HTTP version.
The first part of the request line is the method being used, such as GET or POST, which describes how the client is making this request. Our client used a GET request, which means it is asking for information.
Definition 21.7 (URI). The uniform resource identifier the client is requesting — here /. A URI is almost, but not quite, the same as a uniform resource locator (URL). The difference isn’t important for our purposes; the HTTP spec uses the term URI, so we can mentally substitute URL for URI here.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
14
Chapter 21 · 21.1 Building a Single-Threaded Web Server
HTTP version, CRLF and headers
The last part of the request line is the HTTP version the client uses; then the request line ends in a CRLF sequence.
Definition 21.8 (CRLF sequence). Carriage return and line feed — terms from the typewriter days — written \r\n, where \r is a carriage return and \n is a line feed. It separates the request line from the rest of the request data. When the CRLF is printed, we see a new line start rather than \r\n.
In the request line we received, GET is the method, / is the request URI, and HTTP/1.1 is the version.
After the request line, the remaining lines starting from Host: onward are headers. GET requests have no body.
Try making a request from a different browser, or asking for a different address such as 127.0.0.1:7878/test, to see how the request data changes.
Now that we know what the browser is asking for, let’s send back some data!
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
15
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Writing a response: the format
To send data in response to a client request: responses have the following format.
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
Definition 21.9 (Status line). The first line of a response: the HTTP version used in the response, a numeric status code that summarizes the result of the request, and a reason phrase that gives a text description of the status code. After its CRLF come any headers, another CRLF sequence, and the body of the response.
Example 21.10. HTTP version 1.1, status code 200 (the standard success response), an OK reason phrase, no headers, and no body: a tiny successful HTTP response.
HTTP/1.1 200 OK\r\n\r\n
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
16
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Writing the tiny response to the stream
In handle_connection, remove the println! that printed the request data and replace it:
Listing 21.3: Writing a tiny successful HTTP response to the stream — src/main.rs
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write_all(response.as_bytes()).unwrap();
}
The use lines and main are unchanged from Listing 21.2.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
17
Chapter 21 · 21.1 Building a Single-Threaded Web Server
How the response is sent; returning real HTML
- The first new line defines the response variable, holding the success message’s data.
- We call as_bytes on response to convert the string data to bytes. The write_all method on stream takes a &[u8] and sends those bytes directly down the connection.
- Because write_all could fail, we use unwrap on any error result as before. In a real application, you would add error handling here.
Run the code and make a request. We no longer print anything, so the terminal shows only Cargo’s output. Loading 127.0.0.1:7878 now gives a blank page instead of an error. You’ve just handcoded receiving an HTTP request and sending a response!
Returning real HTML
To return more than a blank page, create the new file hello.html in the root of your project directory, not in the src directory. You can input any HTML you want; Listing 21.4 shows one possibility.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
18
Chapter 21 · 21.1 Building a Single-Threaded Web Server
A sample HTML file
Listing 21.4: A sample HTML file to return in a response — hello.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>
This is a minimal HTML5 document with a heading and some text. To return it from the server when a request is received, we modify handle_connection as in Listing 21.5: read the HTML file, add it to the response as a body, and send it.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
19
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Sending hello.html as the response body
Listing 21.5: Sending the contents of hello.html as the body of the response — src/main.rs
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
// --snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
The // --snip-- stands for main, unchanged.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
20
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Building the response with Content-Length
Listing 21.5 (continued): Sending the contents of hello.html as the body of the response — src/main.rs
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
- We added fs to the use statement to bring the standard library’s filesystem module into scope. Reading a file’s contents to a string should look familiar from the I/O project (Listing 12.4).
- We use format! to add the file’s contents as the body of the success response. To ensure a valid HTTP response, we add the Content-Length header, set to the size of the response body — here, the size of hello.html.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
21
Chapter 21 · 21.1 Building a Single-Threaded Web Server
HTML rendered — but for any request
Run this code with cargo run and load 127.0.0.1:7878 in your browser: you should see your HTML rendered!
Currently, we ignore the request data in http_request and send back the contents of the HTML file unconditionally. Requesting 127.0.0.1:7878/something-else still gets the same HTML response. Our server is very limited and does not do what most web servers do. We want to customize responses depending on the request, and only send back the HTML file for a well-formed request to /.
Validating the request and selectively responding
Let’s check that the browser is requesting / before returning the HTML file, and return an error if the browser requests anything else. Listing 21.6 modifies handle_connection: it checks the content of the request received against what we know a request for / looks like, and adds if and else blocks to treat requests differently.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
22
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Checking the request line
Listing 21.6: Handling requests to / differently from other requests — src/main.rs
// --snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
23
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Reading only the first line
Listing 21.6 (continued) — src/main.rs
} else {
// some other request
}
}
- We only look at the first line, so rather than reading the whole request into a vector, we call next to get the first item. The first unwrap handles the Option, stopping the program if the iterator has no items; the second handles the Result, like the unwrap in the map of Listing 21.2.
- If request_line equals the request line of a GET request to /, the if block returns the contents of our HTML file.
- Otherwise we’ve received some other request; the else block will respond to those in a moment.
Run it: 127.0.0.1:7878 gives the HTML in hello.html; any other request, such as 127.0.0.1:7878/something-else, gets a connection error like those from Listings 21.1 and 21.2.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
24
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Responding with 404 to everything else
The else block now returns status code 404, which signals that the content for the request was not found, plus some HTML for a page to render in the browser for the end user.
Listing 21.7: Responding with status code 404 and an error page if anything other than / was requested — src/main.rs
// --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
25
Chapter 21 · 21.1 Building a Single-Threaded Web Server
The 404 page
The response has a status line with status code 404 and the reason phrase NOT FOUND. Its body is the HTML in 404.html: create that file next to hello.html, with any HTML you want, or this example:
Listing 21.8: Sample content for the page to send back with any 404 response — 404.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>
Run again: 127.0.0.1:7878 returns hello.html; any other request, like 127.0.0.1:7878/foo, returns 404.html.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
26
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Refactoring
The if and else blocks repeat a lot: both read files and write the contents to the stream. The only differences are the status line and the filename. So we pull those differences into if and else lines that assign the status line and filename to variables, then use the variables unconditionally to read the file and write the response.
Listing 21.9: Refactoring the if and else blocks to contain only the code that differs between the two cases — src/main.rs
// --snip--
fn handle_connection(mut stream: TcpStream) {
// --snip--
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")
};
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
27
Chapter 21 · 21.1 Building a Single-Threaded Web Server
Refactoring: the shared code
Listing 21.9 (continued) — src/main.rs
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
- The if and else blocks now only return the appropriate status line and filename in a tuple; we use destructuring to assign them to status_line and filename with a pattern in the let statement (Chapter 19).
- The previously duplicated code is now outside the blocks and uses those variables. The difference between the two cases is easier to see, and there is only one place to update if we change how file reading and response writing work. Behavior is the same as Listing 21.7.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
28
Chapter 21 · 21.1 Building a Single-Threaded Web Server
A web server in about 40 lines
Awesome! We now have a simple web server in approximately 40 lines of Rust code. It:
- listens for TCP connections on 127.0.0.1:7878;
- reads the request line of each HTTP request;
- responds to one request (GET / HTTP/1.1) with a page of content, hello.html, and status 200 OK;
- responds to all other requests with a 404 response and 404.html.
Currently, our server runs in a single thread, meaning it can only serve one request at a time. Next we examine how that can be a problem by simulating some slow requests, and then fix it so that the server can handle multiple requests at once.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
29
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Simulating a slow request
The server processes each request in turn: it won’t process a second connection until the first is finished. With more and more requests, this serial execution is less and less optimal. A request that takes a long time makes subsequent requests wait, even if they could be processed quickly. Listing 21.10 handles a request to /sleep with a simulated slow response: the server sleeps for five seconds before responding.
Listing 21.10: Simulating a slow request by sleeping for five seconds — src/main.rs
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
// --snip--
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
30
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Three cases with match
Listing 21.10 (continued): Simulating a slow request by sleeping for five seconds — src/main.rs
fn handle_connection(mut stream: TcpStream) {
// --snip--
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 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
// --snip--
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
31
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The slow request blocks everyone
- We switched from if to match now that there are three cases. We must explicitly match on a slice of request_line to pattern-match against the string literal values: match doesn’t do automatic referencing and dereferencing, like the equality method does.
- The first arm is the same as the if block from Listing 21.9. The second arm matches a request to /sleep: the server sleeps for five seconds before rendering the successful HTML page. The third arm is the same as the else block from Listing 21.9.
You can see how primitive our server is: real libraries would handle the recognition of multiple requests in a much less verbose way!
Start the server with cargo run and open two browser windows: one for http://127.0.0.1:7878, the other for http://127.0.0.1:7878/sleep. Entering the / URI a few times, it responds quickly. But if you enter /sleep and then load /, / waits until sleep has slept its full five seconds before loading.
There are multiple techniques to avoid requests backing up behind a slow request, including async as in Chapter 17; the one we’ll implement is a thread pool.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
32
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Improving throughput with a thread pool
Definition 21.11 (Thread pool). A group of spawned threads that are ready and waiting to handle a task. When the program receives a new task, it assigns one of the threads in the pool to it, and that thread processes the task. The remaining threads are available for any other tasks that come in meanwhile. When a thread finishes its task, it returns to the pool of idle threads, ready for a new task.
A thread pool lets you process connections concurrently, increasing the throughput of your server.
We limit the number of threads in the pool to a small number to protect against DoS attacks. If the program created a new thread for each request, someone making 10 million requests could wreak havoc by using up all our server’s resources and grinding the processing of requests to a halt.
So, rather than spawning unlimited threads, a fixed number of threads wait in the pool. Incoming requests are sent to the pool, which maintains a queue of them. Each thread pops a request off this queue, handles it, then asks the queue for another. We can process up to N requests concurrently, where N is the number of threads. If every thread is responding to a long-running request, later requests can still back up in the queue — but we’ve increased how many long-running requests we can handle before reaching that point.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
33
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Other models, and how we’ll design the pool
A thread pool is just one of many ways to improve a web server’s throughput. Other options you might explore are the fork/join model, the single-threaded async I/O model, and the multithreaded async I/O model.
If you’re interested, read more about other solutions and try to implement them; with a low-level language like Rust, all of these options are possible.
Interface first, then compiler-driven development
Before implementing the pool, consider what using it should look like. Writing the client interface first can guide your design: write the API so it’s structured the way you want to call it, then implement the functionality within that structure — rather than implementing first and designing the public API afterwards.
Like the test-driven development of Chapter 12, we’ll use compiler-driven development: write the code that calls the functions we want, then let the compiler’s errors tell us what to change next. First, though, we explore the technique we’re not going to use, as a starting point.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
34
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Spawning a thread for each request
First, how might the code look if it created a new thread for every connection? Not our final plan, because of the problems with potentially spawning unlimited threads — but a starting point to get a working multithreaded server. Then we add the thread pool as an improvement, and contrasting the two solutions is easier.
Listing 21.11: Spawning a new thread for each stream — src/main.rs
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);
});
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
35
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Unlimited threads, and a finite alternative
As in Chapter 16, thread::spawn creates a new thread and runs the code in the closure in it. Run this code and load /sleep, then / in two more browser tabs: the requests to / indeed don’t wait for /sleep to finish. But this will eventually overwhelm the system, because it makes new threads without any limit.
Remark. Recall from Chapter 17 that this is exactly the kind of situation where async and await really shine! Keep that in mind as we build the thread pool, and think about how things would look different or the same with async.
Creating a finite number of threads
We want the thread pool to work in a similar, familiar way, so that switching from threads to a thread pool doesn’t require large changes to the code that uses our API. Listing 21.12 shows the hypothetical interface for a ThreadPool struct we want to use instead of thread::spawn.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
36
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The ideal ThreadPool interface
Listing 21.12: Our ideal ThreadPool interface — src/main.rs
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);
});
}
}
ThreadPool::new creates a pool with a configurable number of threads, here four. In the loop, pool.execute, like thread::spawn, takes a closure the pool should run for each stream, and must give it to a thread in the pool. This won’t compile yet, but we try so the compiler can guide us.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
37
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Compiler-driven development: first error
Make the changes of Listing 21.12 to src/main.rs, then let the errors from cargo check drive development. The first error:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: cannot find type `ThreadPool` in this scope
--> src/main.rs:11:16
|
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` (bin "hello") due to 1 previous error
Great! We need a ThreadPool type or module. Its implementation will be independent of the kind of work our web server does, so we switch the hello crate from a binary crate to a library crate to hold ThreadPool. The library is then usable for any thread-pool work, not just web requests.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
38
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
A ThreadPool type in a library crate
Create a src/lib.rs file with the simplest definition of a ThreadPool struct we can have for now. Then bring ThreadPool into scope from the library crate by adding a line to the top of src/main.rs; the rest of src/main.rs stays as in Listing 21.12.
src/lib.rs
pub struct ThreadPool;
src/main.rs
use hello::ThreadPool;
This code still won’t work, but we check it again to get the next error to address.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
39
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Second error: no function new
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no associated function or constant named `new` found for struct `ThreadPool`
in the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ associated function or constant not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Next we need an associated function named new for ThreadPool. We also know new needs one parameter that can accept 4 as an argument, and should return a ThreadPool instance. Let’s implement the simplest new function that has those characteristics.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
40
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The simplest ThreadPool::new
src/lib.rs
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
We chose usize as the type of the size parameter because a negative number of threads doesn’t make any sense. We also know we’ll use this 4 as the number of elements in a collection of threads, which is what the usize type is for, as discussed in “Integer Types” in Chapter 3.
Let’s check the code again.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
41
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Third error: no method execute
$ 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/main.rs:17:14
|
17 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
There’s no execute method yet. As decided, it should resemble thread::spawn: take the closure it’s given and give it to an idle thread in the pool to run.
So execute takes a closure as a parameter. Closures can be taken as parameters with three different traits — Fn, FnMut and FnOnce (Chapter 13) — so which one? We’ll end up doing something similar to the standard library’s thread::spawn, so we can look at the bounds its signature puts on its parameter.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
42
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Learning from thread::spawn’s signature
The documentation shows:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
- We care about F; T relates to the return value, which doesn’t concern us.
- spawn uses FnOnce as the trait bound on F. That’s probably what we want too, because we’ll eventually pass the argument we get in execute to spawn. And the thread running a request will execute that request’s closure only one time, which matches the Once in FnOnce.
- F also has the trait bound Send and the lifetime bound 'static, both useful here: we need Send to transfer the closure from one thread to another, and 'static because we don’t know how long the thread will take to execute.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
43
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
An execute method with the same bounds
We create an execute method on ThreadPool that takes a generic parameter of type F with these bounds:
src/lib.rs
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
- The () after FnOnce: a closure taking no parameters and returning the unit type (). As with functions, the return type can be omitted, but the parentheses are needed even with no parameters.
- Again, this is the simplest implementation: it does nothing, but we’re only trying to make the code compile.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
44
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
It compiles — but does nothing
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
It compiles! But if you try cargo run and make a request in the browser, you’ll see the errors in the browser that we saw at the beginning of the chapter. Our library isn’t actually calling the closure passed to execute yet!
Remark. A saying you might hear about languages with strict compilers, such as Haskell and Rust, is “If the code compiles, it works.” But this saying is not universally true. Our project compiles, but it does absolutely nothing! If we were building a real, complete project, this would be a good time to start writing unit tests to check that the code compiles and has the behavior we want.
Consider: what would be different here if we were going to execute a future instead of a closure?
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
45
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Validating the number of threads in new
We aren’t doing anything with the parameters to new and execute. Starting with new: we chose an unsigned type for size because a negative number of threads makes no sense. But a pool with zero threads also makes no sense, yet zero is a perfectly valid usize. So we check that size is greater than zero before returning a ThreadPool, making the program panic on zero with the assert! macro.
Listing 21.13: Implementing ThreadPool::new to panic if size is zero — src/lib.rs
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
46
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
assert! and documenting panics
Listing 21.13 (continued) — src/lib.rs
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
ThreadPool
}
// --snip--
}
We’ve also documented ThreadPool with doc comments, following good practice by adding a section that calls out the situations in which our function can panic (Chapter 14). Try running cargo doc --open and clicking the ThreadPool struct to see the generated docs for new!
Instead of the assert!, we could change new into build and return a Result, like Config::build in the I/O project (Listing 12.9). But we’ve decided that trying to create a thread pool without any threads should be an unrecoverable error.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
47
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
An exercise; then storing the threads
If you’re feeling ambitious, try to write a function named build with the following signature to compare with the new function:
pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
Creating space to store the threads
Now that we know we have a valid number of threads, we can create them and store them in the ThreadPool struct before returning it. But how do we “store” a thread? Take another look at the thread::spawn signature:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
48
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
A vector of JoinHandles
spawn returns a JoinHandle<T>, where T is the type the closure returns. Let’s try using JoinHandle too. Our closures will handle the connection and not return anything, so T will be the unit type (). Listing 21.14 compiles but doesn’t create any threads yet: ThreadPool now holds a vector of thread::JoinHandle<()>, initialized with a capacity of size; a for loop will run code to create the threads; and the instance containing them is returned.
Listing 21.14: Creating a vector for ThreadPool to hold the threads — src/lib.rs
use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
49
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
A vector of JoinHandles (continued)
Listing 21.14 (continued) — src/lib.rs
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--
}
We brought std::thread into scope in the library crate because we use thread::JoinHandle as the type of the items in the vector in ThreadPool.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
50
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
with_capacity; threads that wait for code
Once a valid size is received, ThreadPool creates a new vector that can hold size items. with_capacity performs the same task as Vec::new, with an important difference: it pre-allocates space in the vector. Because we know we need to store size elements, allocating up front is slightly more efficient than Vec::new, which resizes itself as elements are inserted.
Run cargo check again: it should succeed.
Sending code from the ThreadPool to a thread
Listing 21.14 left a comment in the for loop about creating threads. The standard library provides thread::spawn to create threads, and thread::spawn expects to get some code the thread should run as soon as it is created. But we want to create the threads and have them wait for code that we’ll send later. The standard library’s implementation of threads doesn’t include any way to do that; we have to implement it manually.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
51
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Introducing Worker
We implement this behavior with a new data structure between the ThreadPool and the threads, which manages the new behavior.
Definition 21.12 (Worker). A common term in pooling implementations: a structure that picks up code that needs to be run and runs the code in its thread.
Think of people working in the kitchen at a restaurant: the workers wait until orders come in from customers, and then they’re responsible for taking those orders and filling them.
Instead of a vector of JoinHandle<()> instances, the thread pool will store instances of the Worker struct. Each Worker stores a single JoinHandle<()>. Then we’ll implement a method on Worker that takes a closure of code to run and sends it to the already running thread for execution. Each Worker also gets an id, to distinguish the instances in the pool when logging or debugging.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
52
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The plan for Worker
Here is the new process when we create a ThreadPool. We’ll implement the code that sends the closure to the thread after Worker is set up this way:
- Define a Worker struct that holds an id and a JoinHandle<()>.
- Change ThreadPool to hold a vector of Worker instances.
- Define a Worker::new function that takes an id number and returns a Worker instance holding the id and a thread spawned with an empty closure.
- In ThreadPool::new, use the for loop counter to generate an id, create a new Worker with that id, and store the Worker in the vector.
If you’re up for a challenge, try implementing these changes on your own before looking at the code in Listing 21.15.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
53
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
ThreadPool holds Worker instances
Listing 21.15: Modifying ThreadPool to hold Worker instances instead of holding threads directly — src/lib.rs
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 }
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
54
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
What changed in Listing 21.15
- The field on ThreadPool is renamed from threads to workers, because it now holds Worker instances instead of JoinHandle<()> instances.
- The counter in the for loop is the argument to Worker::new, and each new Worker is stored in the vector named workers.
- External code (like our server in src/main.rs) doesn’t need to know that ThreadPool uses a Worker struct, so Worker and its new function are private.
- Worker::new uses the id we give it and stores a JoinHandle<()> created by spawning a new thread with an empty closure.
Remark. If the operating system can’t create a thread because there aren’t enough system resources, thread::spawn will panic. That causes our whole server to panic, even though the creation of some threads might succeed. For simplicity this is fine, but a production thread pool would likely use std::thread::Builder and its spawn method, which returns Result instead.
This compiles and stores the number of Worker instances we passed to ThreadPool::new. But we’re still not processing the closure we get in execute.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
55
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Sending requests to threads via channels
The closures given to thread::spawn do absolutely nothing. We get the closure to execute in execute, but we must give thread::spawn a closure when we create each Worker during the creation of the ThreadPool. We want each Worker to fetch the code to run from a queue held in the ThreadPool and send it to its thread to run.
The channels of Chapter 16 — a simple way to communicate between two threads — are perfect for this. A channel functions as the queue of jobs, and execute sends a job from the ThreadPool to the Worker instances, which send it to their thread. The plan:
- The ThreadPool creates a channel and holds on to the sender.
- Each Worker holds on to the receiver.
- A new Job struct holds the closures we want to send down the channel.
- The execute method sends the job it wants to execute through the sender.
- In its thread, the Worker loops over its receiver and executes the closures of any jobs it receives.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
56
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The pool creates a channel and keeps the sender
We create a channel in ThreadPool::new and hold the sender in the instance. Job holds nothing for now but will be the type of item we send down the channel.
Listing 21.16: Modifying ThreadPool to store the sender of a channel that transmits Job instances — src/lib.rs
use std::{sync::mpsc, thread};
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--
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
57
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Passing the receiver to each Worker
Listing 21.16 compiles. Next, let’s pass a receiver of the channel into each Worker as the pool creates the channel. We want to use the receiver in the thread the Worker spawns, so we reference the receiver parameter in the closure. This won’t quite compile yet.
Listing 21.17: Passing the receiver to each Worker — src/lib.rs
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--
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
58
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Worker::new takes the receiver
Listing 21.17 (continued): Passing the receiver to each Worker — src/lib.rs
// --snip--
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
These are small and straightforward changes: we pass the receiver into Worker::new, and then we use it inside the closure. When we try to check this code, we get the error on the next slide.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
59
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Error E0382: use of moved value receiver
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type
| `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
60
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Why one receiver can’t go to many Workers
note: consider changing this parameter type in method `new` to borrow instead if owning the value
isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error
The code tries to pass receiver to multiple Worker instances. As in Chapter 16, Rust’s channel implementation is multiple producer, single consumer, so we can’t just clone the consuming end. Nor do we want to send a message multiple times to multiple consumers: we want one list of messages with multiple Worker instances, such that each message gets processed once.
Also, taking a job off the channel queue mutates the receiver, so the threads need a safe way to share and modify it; otherwise we might get race conditions (Chapter 16).
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
61
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Sharing the receiver with Arc<Mutex<T>>
Recall the thread-safe smart pointers of Chapter 16: to share ownership across multiple threads and allow the threads to mutate the value, we use Arc<Mutex<T>>. Arc lets multiple Worker instances own the receiver, and Mutex ensures that only one Worker gets a job from the receiver at a time.
Listing 21.18: Sharing the receiver among the Worker instances using Arc and Mutex — src/lib.rs
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
// --snip--
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
62
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Cloning the Arc for each Worker
Listing 21.18 (continued) — src/lib.rs
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--
}
// --snip--
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
63
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Worker::new takes the shared receiver
Listing 21.18 (continued) — src/lib.rs
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
}
}
In ThreadPool::new, we put the receiver in an Arc and a Mutex. For each new Worker, we clone the Arc to bump the reference count, so that the Worker instances can share ownership of the receiver.
With these changes, the code compiles! We’re getting there!
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
64
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Implementing the execute method
Finally, execute. We also change Job from a struct to a type alias for a trait object that holds the type of closure execute receives. As in “Type Synonyms and Type Aliases” (Chapter 20), type aliases make long types shorter for ease of use.
Listing 21.19: Creating a Job type alias for a Box that holds each closure and then sending the job down the channel — src/lib.rs
// --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--
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
65
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Sending the job — and receiving it
After creating a new Job instance from the closure we get in execute, we send that job down the sending end of the channel.
- We call unwrap on send for the case that sending fails. That might happen if, for example, we stop all our threads from executing, meaning the receiving end has stopped receiving new messages.
- At the moment we can’t stop our threads from executing: they continue as long as the pool exists. We use unwrap because we know the failure case won’t happen, but the compiler doesn’t know that.
But we’re not quite done yet! In the Worker, the closure passed to thread::spawn still only references the receiving end of the channel. Instead, the closure needs to loop forever, asking the receiving end of the channel for a job and running the job when it gets one. Listing 21.20 makes that change to Worker::new.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
66
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The Worker loop
Listing 21.20: Receiving and executing the jobs in the Worker instance’s thread — src/lib.rs
// --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 {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
67
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
lock, recv, and blocking
let job = receiver.lock().unwrap().recv().unwrap();
First we call lock on the receiver to acquire the mutex, then unwrap to panic on any errors.
Definition 21.13 (Poisoned mutex). The state a mutex is left in when some other thread panicked while holding the lock rather than releasing it. Acquiring a lock on a poisoned mutex fails.
- On a poisoned mutex, calling unwrap to have this thread panic is the correct action. Feel free to change it to an expect with an error message that is meaningful to you.
- With the lock, we call recv to receive a Job from the channel. A final unwrap moves past any errors here too, which might occur if the thread holding the sender has shut down — just as send returns Err if the receiver shuts down.
- The call to recv blocks: if there is no job yet, the current thread waits until one becomes available. The Mutex<T> ensures that only one Worker thread at a time is trying to request a job.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
68
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
The pool works: cargo run
Our thread pool is now in a working state! Give it a cargo run and make some requests. First come two warnings:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
7 | workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: fields `id` and `thread` are never read
--> src/lib.rs:48:5
|
47 | struct Worker {
| ------ fields in this struct
48 | id: usize,
| ^^
49 | thread: thread::JoinHandle<()>,
| ^^^^^^
The warnings say we never directly use the workers, id and thread fields — a reminder that we’re not cleaning anything up yet (section 21.3).
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
69
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Workers taking jobs
warning: `hello` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
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.
Success! The thread pool executes connections asynchronously. Never more than four threads are created, so the system won’t get overloaded under many requests. A request to /sleep no longer blocks: another thread serves the other requests.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
70
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Browsers, futures, and a while let question
Remark. If you open /sleep in multiple browser windows simultaneously, they might load one at a time in five-second intervals. Some web browsers execute multiple instances of the same request sequentially for caching reasons. This limitation is not caused by our web server.
This is a good time to pause and consider how the code in Listings 21.18, 21.19 and 21.20 would differ if we used futures instead of a closure for the work to be done. What types would change? How would the method signatures be different, if at all? What parts of the code would stay the same?
After learning about the while let loop in Chapters 17 and 19, you might wonder why we didn’t write the Worker thread code as in Listing 21.21:
while let Ok(job) = receiver.lock().unwrap().recv() {
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
71
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
An alternative Worker loop with while let
Listing 21.21: An alternative implementation of Worker::new using while let — src/lib.rs
// --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 {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
It compiles and runs, but a slow request still makes other requests wait.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
72
Chapter 21 · 21.2 From a Single-Threaded to a Multithreaded Server
Why while let holds the lock too long
The reason is somewhat subtle. The Mutex struct has no public unlock method, because ownership of the lock is based on the lifetime of the MutexGuard<T> within the LockResult<MutexGuard<T>> that the lock method returns. At compile time, the borrow checker can then enforce the rule that a resource guarded by a Mutex cannot be accessed unless we hold the lock. But this can also result in the lock being held longer than intended if we aren’t mindful of the lifetime of the MutexGuard<T>.
let (Listing 21.20) — works
With let job = receiver.lock().unwrap().recv().unwrap();, any temporary values used in the expression on the right of the equal sign are dropped immediately when the let statement ends.
while let (Listing 21.21) — doesn’t
while let (and if let and match) does not drop temporary values until the end of the associated block. The lock stays held during the call to job(), so other Worker instances cannot receive jobs.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
73
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Graceful shutdown and cleanup
Listing 21.20 responds to requests asynchronously through a thread pool, as intended. The warnings about the workers, id and thread fields we don’t use directly remind us that we’re not cleaning anything up. When we halt the main thread with the less elegant ctrl-C method, all other threads stop immediately as well, even in the middle of serving a request.
Next, then:
- implement the Drop trait to call join on each thread in the pool, so they can finish the requests they’re working on before closing;
- implement a way to tell the threads they should stop accepting new requests and shut down;
- to see it in action, modify the server to accept only two requests before gracefully shutting down its thread pool.
Remark. None of this affects the parts of the code that handle executing the closures, so everything here would be the same if we were using a thread pool for an async runtime.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
74
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Implementing Drop on ThreadPool
When the pool is dropped, the threads should join to finish their work. First attempt (won’t quite work yet):
Listing 21.22: Joining each thread when the thread pool goes out of scope — src/lib.rs
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
We loop through each of the pool’s workers, using &mut because self is a mutable reference and we also need to be able to mutate worker. For each one, we print a message that this Worker is shutting down, then call join on its thread. If join fails, unwrap makes Rust panic and go into an ungraceful shutdown.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
75
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Error E0507: cannot move out of worker.thread
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
--> src/lib.rs:52:13
|
52 | worker.thread.join().unwrap();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`,
| which does not implement the `Copy` trait
|
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
--> /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/std/src/thread/join_handle.rs:149:16
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
76
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
join needs ownership of the thread
We can’t call join, because we only have a mutable borrow of each worker, and join takes ownership of its argument. We need to move the thread out of the Worker instance that owns thread, so that join can consume it.
One way: Option and take (as in Listing 18.15)
If Worker held an Option<thread::JoinHandle<()>>, we could call take on the Option to move the value out of the Some variant and leave None in its place. A running Worker would have Some in thread; to clean it up, we’d replace Some with None, so the Worker wouldn’t have a thread to run.
However, the only time this would come up is when dropping the Worker. In exchange, we’d have to deal with an Option<thread::JoinHandle<()>> anywhere we accessed worker.thread. Idiomatic Rust uses Option quite a bit, but when you find yourself wrapping something you know will always be present in an Option as a workaround like this, it’s a good idea to look for alternative approaches that make your code cleaner and less error-prone.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
77
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
A better alternative: Vec::drain
Vec::drain takes a range of items to remove and returns an iterator of them; the .. range removes every value. We update drop in src/lib.rs:
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
This resolves the compiler error and requires no other changes to our code.
Remark. Because drop can be called when panicking, the unwrap could also panic and cause a double panic, which immediately crashes the program and ends any cleanup in progress. This is fine for an example program, but it isn’t recommended for production code.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
78
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Signaling the threads to stop listening for jobs
With these changes, the code compiles without any warnings. The bad news: it doesn’t function the way we want yet. The key is the logic in the closures run by the Worker threads. We call join, but that won’t shut down the threads, because they loop forever looking for jobs. If we drop our ThreadPool with the current drop, the main thread will block forever, waiting for the first thread to finish.
The fix needs two changes:
- in the ThreadPool drop implementation, explicitly drop the sender before waiting for the threads to finish (Listing 21.23);
- in the Worker loop, exit gracefully when the channel is closed (Listing 21.24).
Unlike with the thread, here we do need an Option, to be able to move sender out of ThreadPool with Option::take.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
79
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
The sender becomes an Option
Listing 21.23: Explicitly dropping sender before joining the Worker threads — src/lib.rs
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
// --snip--
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
// --snip--
ThreadPool {
workers,
sender: Some(sender),
}
}
ThreadPool::new now stores the sender wrapped in Some.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
80
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
execute reaches the sender through the Option
Listing 21.23 (continued) — src/lib.rs
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
Because sender is now an Option, execute borrows the sender with as_ref and unwraps it before sending the job.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
81
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Dropping the sender closes the channel
Listing 21.23 (continued) — src/lib.rs
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
Dropping sender closes the channel, which indicates no more messages will be sent. When that happens, all the calls to recv that the Worker instances make in the infinite loop return an error. Listing 21.24 changes the Worker loop to gracefully exit the loop in that case, so the threads finish when the ThreadPool drop implementation calls join on them.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
82
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Breaking out of the Worker loop
Listing 21.24: Explicitly breaking out of the loop when recv returns an error — src/lib.rs
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
83
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Breaking out of the Worker loop (continued)
Listing 21.24 (continued) — src/lib.rs
}
});
Worker { id, thread }
}
}
On Ok(job) the Worker runs the job as before; on Err — the channel has been closed — it prints that it disconnected and breaks out of the loop, so its thread finishes.
To see this code in action, Listing 21.25 modifies main to accept only two requests before gracefully shutting down the server.
Remark. You wouldn’t want a real-world web server to shut down after serving only two requests. This code just demonstrates that the graceful shutdown and cleanup is in working order.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
84
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Serving two requests, then shutting down
Listing 21.25: Shutting down the server after serving two requests by exiting the loop — src/main.rs
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.");
}
The take method, defined in the Iterator trait, limits the iteration to the first two items at most. The ThreadPool goes out of scope at the end of main, and the drop implementation runs.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
85
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Watching the graceful shutdown
Start the server with cargo run and make three requests. The third request should error, and the terminal shows output similar to this:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
86
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Reading the shutdown output
You might see a different ordering of Worker IDs and messages. From the messages: Worker instances 0 and 3 got the first two requests. The server stopped accepting connections after the second connection, and the Drop implementation on ThreadPool starts executing before Worker 3 even starts its job. Dropping the sender disconnects all the Worker instances and tells them to shut down. Each prints a message when it disconnects, and then the thread pool calls join to wait for each Worker thread to finish.
One interesting aspect of this particular execution: the ThreadPool dropped the sender, and before any Worker received an error, we tried to join Worker 0. Worker 0 had not yet gotten an error from recv, so the main thread blocked, waiting for it to finish. Meanwhile, Worker 3 received a job, and then all threads received an error. When Worker 0 finished, the main thread waited for the rest; by then they had all exited their loops and stopped.
Congrats! The project is complete: a basic web server that uses a thread pool to respond asynchronously, with a graceful shutdown that cleans up all the threads in the pool.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
87
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/main.rs (1 of 3)
Here’s the full code for reference.
src/main.rs
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
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().take(2) {
let stream = stream.unwrap();
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
88
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/main.rs (2 of 3)
src/main.rs (continued)
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
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) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
89
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/main.rs (3 of 3)
src/main.rs (continued)
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
90
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/lib.rs (1 of 6)
src/lib.rs
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
91
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/lib.rs (2 of 6)
src/lib.rs (continued)
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
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)));
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
92
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/lib.rs (3 of 6)
src/lib.rs (continued)
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
93
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/lib.rs (4 of 6)
src/lib.rs (continued)
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
struct Worker {
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
94
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/lib.rs (5 of 6)
src/lib.rs (continued)
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
95
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Full code: src/lib.rs (6 of 6)
src/lib.rs (continued)
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
96
Chapter 21 · 21.3 Graceful Shutdown and Cleanup
Ideas for enhancing the project
We could do more here! If you want to continue enhancing this project, here are some ideas:
- Add more documentation to ThreadPool and its public methods.
- Add tests of the library’s functionality.
- Change calls to unwrap to more robust error handling.
- Use ThreadPool to perform some task other than serving web requests.
- Find a thread pool crate on crates.io and implement a similar web server using the crate instead. Then compare its API and robustness to the thread pool we implemented.
Remark. In this reference src/lib.rs, Worker holds an Option<thread::JoinHandle<()>> and drop joins each thread via take — the Option approach discussed earlier — rather than the Vec::drain version of Listings 21.23–21.24. Both shut down the same way.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
97
Chapter 21 · Summary
Chapter summary
Well done! You’ve made it to the end of the book. In this final project you:
- listened for TCP connections with TcpListener, read HTTP request lines through a BufReader, and wrote raw HTTP responses (status line, Content-Length, body) to the TcpStream;
- served hello.html for / and 404.html for anything else, and saw how a slow /sleep request blocks a single-threaded server;
- built a ThreadPool by compiler-driven development: Workers fed through a channel whose receiver is shared as Arc<Mutex<Receiver<Job>>>;
- shut the pool down gracefully with Drop: dropping the sender closes the channel, the Workers break out of their loops, and join waits for them.
Thank you for joining this tour of Rust. You’re now ready to implement your own Rust projects and help with other people’s.
Remark. This is the last chapter; the appendices follow as reference material. Keep in mind that there is a welcoming community of other Rustaceans who would love to help you with any challenges you encounter on your Rust journey.
The Rust Programming Language · Ch.21 Final Project: Building a Multithreaded Web Server
98