Designing the Thread Pool Interface
Let’s talk about what using the pool should look like. The authors often find that when trying to design some code, writing the client interface first can really help guide your design. Write the API of the code to be structured in the way you’d want to call it, then implement the functionality within that structure rather than implementing the functionality then designing the public API.
Similar to how we used Test Driven Development in the project in Chapter 12, we’re going to use Compiler Driven Development here. We’re going to write the code that calls the functions we wish we had, then we’ll lean on the compiler to tell us what we should change next. The compiler error messages will guide our implementation.
Code Structure if We Could Use thread::spawn
First, let’s explore what the code to create a new thread for every connection
could look like. This isn’t our final plan due to the problems with potentially
spawning an unlimited number of threads that we talked about earlier, but it’s
a start. Listing 20-11 shows the changes to main
to spawn a new thread to
handle each stream within the for
loop:
Filename: src/main.rs
# use std::thread; # use std::io::prelude::*; # use std::net::TcpListener; # use std::net::TcpStream; # fn main() { let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(|| { handle_connection(stream); }); } } # fn handle_connection(mut stream: TcpStream) {}
As we learned in Chapter 16, thread::spawn
will create a new thread and then
run the code in the closure in it. If you run this code and load /sleep
and
then /
in two browser tabs, you’ll indeed see the request to /
doesn’t have
to wait for /sleep
to finish. But as we mentioned, this will eventually
overwhelm the system since we’re making new threads without any limit.
Creating a Similar Interface for ThreadPool
We want our 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 we want
to run in the pool. Listing 20-12 shows the hypothetical interface for a
ThreadPool
struct we’d like to use instead of thread::spawn
:
Filename: src/main.rs
# use std::thread; # use std::io::prelude::*; # use std::net::TcpListener; # use std::net::TcpStream; # struct ThreadPool; # impl ThreadPool { # fn new(size: u32) -> ThreadPool { ThreadPool } # fn execute<F>(&self, f: F) # where F: FnOnce() + Send + 'static {} # } # fn main() { let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming() { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } } # fn handle_connection(mut stream: TcpStream) {}
We use ThreadPool::new
to create a new thread pool with a configurable number
of threads, in this case four. Then, in the for
loop, pool.execute
will
work in a similar way to thread::spawn
.
Compiler Driven Development to Get the API Compiling
Go ahead and make the changes in Listing 20-12 to src/main.rs, and let’s use the compiler errors to drive our development. Here’s the first error we get:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve. Use of undeclared type or module `ThreadPool`
--> src\main.rs:10:16
|
10 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^^^^^^ Use of undeclared type or module
`ThreadPool`
error: aborting due to previous error
Great, we need a ThreadPool
. Let’s switch the hello
crate from a binary
crate to a library crate to hold our ThreadPool
implementation, since the
thread pool implementation will be independent of the particular kind of work
that we’re doing in our web server. Once we’ve got the thread pool library
written, we could use that functionality to do whatever work we want to do, not
just serve web requests.
So create src/lib.rs that contains the simplest definition of a ThreadPool
struct that we can have for now:
Filename: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub struct ThreadPool; #}
Then create a new directory, src/bin, and move the binary crate rooted in
src/main.rs into src/bin/main.rs. This will make the library crate be the
primary crate in the hello directory; we can still run the binary in
src/bin/main.rs using cargo run
though. After moving the main.rs file,
edit it to bring the library crate in and bring ThreadPool
into scope by
adding this at the top of src/bin/main.rs:
Filename: src/bin/main.rs
extern crate hello;
use hello::ThreadPool;
And try again in order to get the next error that we need to address:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error: no associated item named `new` found for type `hello::ThreadPool` in the
current scope
--> src\main.rs:13:16
|
13 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^^^^^^
|
Cool, the next thing is to create an associated function named new
for
ThreadPool
. We also know that new
needs to have one parameter that can
accept 4
as an argument, and new
should return a ThreadPool
instance.
Let’s implement the simplest new
function that will have those
characteristics:
Filename: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub struct ThreadPool; impl ThreadPool { pub fn new(size: u32) -> ThreadPool { ThreadPool } } #}
We picked u32
as the type of the size
parameter, since we know that a
negative number of threads makes no sense. u32
is a solid default. Once we
actually implement new
for real, we’ll reconsider whether this is the right
choice for what the implementation needs, but for now, we’re just working
through compiler errors.
Let’s check the code again:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`, #[warn(unused_variables)] on by default
--> src/lib.rs:4:16
|
4 | pub fn new(size: u32) -> ThreadPool {
| ^^^^
error: no method named `execute` found for type `hello::ThreadPool` in the
current scope
--> src/main.rs:18:14
|
18 | pool.execute(|| {
| ^^^^^^^
Okay, a warning and an error. Ignoring the warning for a moment, the error is
because we don’t have an execute
method on ThreadPool
. Let’s define one,
and we need it to take a closure. If you remember from Chapter 13, we can take
closures as arguments with three different traits: Fn
, FnMut
, and FnOnce
.
What kind of closure should we use? Well, we know we’re going to end up doing
something similar to thread::spawn
; what bounds does the signature of
thread::spawn
have on its argument? Let’s look at the documentation, which
says:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static
F
is the parameter we care about here; T
is related to the return value and
we’re not concerned with that. Given that spawn
uses FnOnce
as the trait
bound on F
, it’s probably what we want as well, since we’ll eventually be
passing the argument we get in execute
to spawn
. We can be further
confident that FnOnce
is the trait that we want to use since the thread for
running a request is only going to execute that request’s closure one time.
F
also has the trait bound Send
and the lifetime bound 'static
, which
also make sense for our situation: we need Send
to transfer the closure from
one thread to another, and 'static
because we don’t know how long the thread
will execute. Let’s create an execute
method on ThreadPool
that will take a
generic parameter F
with these bounds:
Filename: src/lib.rs
# #![allow(unused_variables)] #fn main() { # pub struct ThreadPool; impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static { } } #}
The FnOnce
trait still needs the ()
after it since this FnOnce
is
representing a closure that takes no parameters and doesn’t return a value.
Just like function definitions, the return type can be omitted from the
signature, but even if we have no parameters, we still need the parentheses.
Again, since we’re working on getting the interface compiling, we’re adding the
simplest implementation of the execute
method, which does nothing. Let’s
check again:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`, #[warn(unused_variables)] on by default
--> src/lib.rs:4:16
|
4 | pub fn new(size: u32) -> ThreadPool {
| ^^^^
warning: unused variable: `f`, #[warn(unused_variables)] on by default
--> src/lib.rs:8:30
|
8 | pub fn execute<F>(&self, f: F)
| ^
Only warnings now! It compiles! Note that if you try cargo run
and making a
request in the browser, though, you’ll see the errors in the browser again that
we saw in the beginning of the chapter. Our library isn’t actually calling the
closure passed to execute
yet!
A saying you might hear about languages with strict compilers like Haskell and Rust is “if the code compiles, it works.” This is a good time to remember that this is just a phrase and a feeling people sometimes have, it’s not actually universally true. Our project compiles, but it does absolutely nothing! If we were building a real, complete project, this would be a great time to start writing unit tests to check that the code compiles and has the behavior we want.