How to avoid the std::option::Option workaround in the book 2nd edition 20.3?

Hello.

In “The Rust Programming Language second edition”, in the subchapter 20.3 “Graceful Shutdown and Cleanup”, the listing 20-23 has a code which does not compile (which is normal):

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap(); // COMPILATION ERROR (which is normal)
        }
    }
}

The solution proposed by the book is to replace the struct Worker and the implementation of ThreadPool::drop with:

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

But I think that this workaround increases the memory consumption (due to the std::option::Option overhead) and decreases the compiler controls (because the “thread” member has a new possible value: None).

Is this workaround avoidable?

If no, I thought about a new feature: a customizable destructor.

This feature would allow to replace the normal destruction of the members of a struct with a call to a customizable function which takes ownership of the members.

Pseudocode example:

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

impl ThreadPool {
    fn dtor(workers: Vec<Worker>, _sender: mpsc::Sender<Message>)
    {
        for worker in workers.into_iter() {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

In my pseudocode example, every destruction of a ThreadPool object would be replaced by a call to the destructor ThreadPool::dtor which takes ownership of the variable members of the ThreadPool object.

Another solution would be to let the destructor of Worker to call join:

impl Worker {
    fn dtor(id: usize, thread: thread::JoinHandle<()>)
    {
        println!("Shutting down worker {}", id);
        thread.join().unwrap();
    }
}

But I don’t know Rust well yet (I just finished reading the book), so I am not ready to write a Rust RFC yet.

1 Like

Actually, it doesn't! All the compiler needs is some room to tag Some/None, and it can use "niche" space for this. Basically, if there are some bits of the Some(T) that would be invalid values, it will use that to mean None.

I'm not sure what it's actually using in this case, but one possibility is the JoinHandle -> JoinInner -> thread: Thread -> Arc -> ptr: NonNull -- it could just use a null ptr to indicate an overall None.

You can see the sizes for yourself like:

    println!("{}", std::mem::size_of::<JoinHandle<()>>());
    println!("{}", std::mem::size_of::<Option<JoinHandle<()>>>());

which both print 32 on Linux x86_64.

and decreases the compiler controls (because the “thread” member has a new possible value: None).

I'm more sympathetic to this -- you don't really want to deal with Some vs. None in most cases. I doubt it will make any difference to the performance of the code though.

Here's an alternate possibility for this example, which does work by moving out of the vector:

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}
5 Likes

The real solution to this would be to add &move/&out (or whatever we want to call them) references to the language. Then the signature of Drop::drop could be changed to Drop::drop(&move self).

1 Like

@cuviper : Thanks.

Besides, I realized that my customizable destructor proposal is inconsistent with the current rules of Rust, because one can already takes ownership of a member variable of an object.

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
            // Worker::dtor cannot be called, because join already
            // took ownership of the "thread" member.
        }
    }
}

@canndrew : How fn foo(&move self) would be different from fn foo(self) ?
Drop::drop cannot get its parameter by value, because an infinite recursion could easily happen: in drop, if we call a function with the self parameter by value, this function would indirectly call drop with the same object (self).