Creating the Thread Pool and Storing Threads

The warnings are because we aren’t doing anything with the parameters to new and execute. Let’s implement the bodies of both of these with the actual behavior we want.

Validating the Number of Threads in the Pool

To start, let’s think about new. We mentioned before that we picked an unsigned type for the size parameter since a pool with a negative number of threads makes no sense. However, a pool with zero threads also makes no sense, yet zero is a perfectly valid u32. Let’s check that size is greater than zero before we return a ThreadPool instance and panic if we get zero by using the assert! macro as shown in Listing 20-13:

Filename: src/lib.rs

# pub struct ThreadPool;
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.
    pub fn new(size: u32) -> ThreadPool {
        assert!(size > 0);

        ThreadPool
    }

    // ...snip...
}

Listing 20-13: Implementing ThreadPool::new to panic if size is zero

We’ve taken this opportunity to add some documentation for our ThreadPool with doc comments. Note that we followed good documentation practices and added a section that calls out the situations in which our function can panic as we discussed in Chapter 14. Try running cargo doc --open and clicking on the ThreadPool struct to see what the generate docs for new look like!

Instead of adding the use of the assert! macro as we’ve done here, we could make new return a Result instead like we did with Config::new in the I/O project in Listing 12-9, but we’ve decided in this case that trying to create a thread pool without any threads should be an unrecoverable error. If you’re feeling ambitious, try to write a version of new with this signature to see how you feel about both versions:

fn new(size: u32) -> Result<ThreadPool, PoolCreationError> {

Storing Threads in the Pool

Now that we know we have a valid number of threads to store in the pool, we can actually create that many threads and store them in the ThreadPool struct before returning it.

This raises a question: how do we “store” a thread? Let’s take another look at the signature of thread::spawn:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T + Send + 'static,
        T: Send + 'static

spawn returns a JoinHandle<T>, where T is the type that’s returned from the closure. Let’s try using JoinHandle too and see what happens. In our case, the closures we’re passing to the thread pool will handle the connection and not return anything, so T will be the unit type ().

This won’t compile yet, but let’s consider the code shown in Listing 20-14. We’ve changed the definition of ThreadPool to hold a vector of thread::JoinHandle<()> instances, initialized the vector with a capacity of size, set up a for loop that will run some code to create the threads, and returned a ThreadPool instance containing them:

Filename: src/lib.rs

use std::thread;

pub struct ThreadPool {
    threads: Vec<thread::JoinHandle<()>>,
}

impl ThreadPool {
    // ...snip...
    pub fn new(size: u32) -> 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...
}

Listing 20-14: Creating a vector for ThreadPool to hold the threads

We’ve brought std::thread into scope in the library crate, since we’re using thread::JoinHandle as the type of the items in the vector in ThreadPool.

After we have a valid size, we’re creating a new vector that can hold size items. We haven’t used with_capacity in this book yet; it does the same thing as Vec::new, but with an important difference: it pre-allocates space in the vector. Since we know that we need to store size elements in the vector, doing this allocation up-front is slightly more efficient than only writing Vec::new, since Vec::new resizes itself as elements get inserted. Since we’ve created a vector the exact size that we need up front, no resizing of the underlying vector will happen while we populate the items.

That is, if this code works, which it doesn’t quite yet! If we check this code, we get an error:

$ cargo check
   Compiling hello v0.1.0 (file:///projects/hello)
error[E0308]: mismatched types
  --> src\main.rs:70:46
   |
70 |         let mut threads = Vec::with_capacity(size);
   |                                              ^^^^ expected usize, found u32

error: aborting due to previous error

size is a u32, but Vec::with_capacity needs a usize. We have two options here: we can change our function’s signature, or we can cast the u32 as a usize. If you remember when we defined new, we didn’t think too hard about what number type made sense, we just chose one. Let’s give it some more thought now. Given that size is the length of a vector, usize makes a lot of sense. They even almost share a name! Let’s change the signature of new, which will get the code in Listing 20-14 to compile:

fn new(size: usize) -> ThreadPool {

If run cargo check again, you’ll get a few more warnings, but it should succeed.

We left a comment in the for loop in Listing 20-14 regarding the creation of threads. How do we actually create threads? This is a tough question. What should go in these threads? We don’t know what work they need to do at this point, since the execute method takes the closure and gives it to the pool.

Let’s refactor slightly: instead of storing a vector of JoinHandle<()> instances, let’s create a new struct to represent the concept of a worker. A worker will be what receives a closure in the execute method, and it will take care of actually calling the closure. In addition to letting us store a fixed size number of Worker instances that don’t yet know about the closures they’re going to be executing, we can also give each worker an id so we can tell the different workers in the pool apart when logging or debugging.

Let’s make these changes:

  1. Define a Worker struct that holds an id and a JoinHandle<()>
  2. Change ThreadPool to hold a vector of Worker instances
  3. Define a Worker::new function that takes an id number and returns a Worker instance with that id and a thread spawned with an empty closure, which we’ll fix soon
  4. 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 taking a look at the code in Listing 20-15.

Ready? Here’s Listing 20-15 with one way to make these modifications:

Filename: 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,
        }
    }
}

Listing 20-15: Modifying ThreadPool to hold Worker instances instead of threads directly

We’ve chosen to change the name of the field on ThreadPool from threads to workers since we’ve changed what we’re holding, which is now Worker instances instead of JoinHandle<()> instances. We use the counter in the for loop as an argument to Worker::new, and we store each new Worker in the vector named workers.

The Worker struct and its new function are private since external code (like our server in src/bin/main.rs) doesn’t need to know the implementation detail that we’re using a Worker struct within ThreadPool. The Worker::new function uses the given id and stores a JoinHandle<()> created by spawning a new thread using an empty closure.

This code compiles and is storing the number of Worker instances that we specified as an argument to ThreadPool::new, but we’re still not processing the closure that we get in execute. Let’s talk about how to do that next.

results matching ""

    No results matching ""