Building ThreadPool Using Compiler-Driven Development
Make the changes in Listing 20-12 to src/main.rs, and then let's use the compiler errors from cargo check to drive our development. Here is the first error we get:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
Great! This error tells us we need a ThreadPool type or module, so we'll build one now. Our ThreadPool implementation will be independent of the kind of work our web server is doing. So let's switch the hello crate from a binary crate to a library crate to hold our ThreadPool implementation. After we change to a library crate, we could also use the separate thread pool library for any work we want to do using a thread pool, not just for serving web requests.
Create a src/lib.rs file that contains the following, which is the simplest definition of a ThreadPool struct that we can have for now:
Filename: src/lib.rs
pub struct ThreadPool;
Then edit the main.rs file to bring ThreadPool into scope from the library crate by adding the following code to the top of src/main.rs:
Filename: src/main.rs
use hello::ThreadPool;
This code still won't work, but let's check it again to get the next error that we need to address:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct
`ThreadPool` in the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in
`ThreadPool`
This error indicates that next we need 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 should return a ThreadPool instance. Let's implement the simplest new function that will have those characteristics:
Filename: 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 we know that 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".
Let's check the code again:
$ 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`
Now the error occurs because we don't have an execute method on ThreadPool. Recall from "Creating a Finite Number of Threads" that we decided our thread pool should have an interface similar to thread::spawn. In addition, we'll implement the execute function so it takes the closure it's given and gives it to an idle thread in the pool to run.
We'll define the execute method on ThreadPool to take a closure as a parameter. Recall from "Moving Captured Values Out of Closures and the Fn Traits" that we can take closures as parameters with three different traits: Fn, FnMut, and FnOnce. We need to decide which kind of closure to use here. We know we'll end up doing something similar to the standard library thread::spawn implementation, so we can look at what bounds the signature of thread::spawn has on its parameter. The documentation shows us the following:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
The F type parameter is the one we're concerned with here; the T type parameter is related to the return value, and we're not concerned with that. We can see that spawn uses FnOnce as the trait bound on F. This is probably what we want as well, because we'll eventually pass the argument we get in execute to spawn. We can be further confident that FnOnce is the trait we want to use because the thread for running a request will only execute that request's closure one time, which matches the Once in FnOnce.
The F type parameter also has the trait bound Send and the lifetime bound 'static, which are useful in 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 take to execute. Let's create an execute method on ThreadPool that will take a generic parameter of type F with these bounds:
Filename: src/lib.rs
impl ThreadPool {
--snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() 1 + Send + 'static,
{
}
}
We still use the () after FnOnce [1] because this FnOnce represents a closure that takes no parameters and returns the unit type (). 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, this is the simplest implementation of the execute method: it does nothing, but we're only trying to make our code compile. Let's check it again:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
It compiles! But note that 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!
Note: 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.