Easy Object Pool Management in Rust: Automating Release for Efficiency

Photo by Pixabay: https://www.pexels.com/photo/close-up-photo-of-swimming-rope-261185/

Introduction

In a previous article, we discussed the implementation of a thread-pool. One problem with this implementation is that we had to release objects to the pool manually. Since Rust has ownership, a very powerfull tool I am beginning to find, it also has Drop semantics, which we can use to automate this release, once a pool object gets dropped, for example when it goes out of scope.

Implementation in Rust

For this example we will return to the garage for expensive cars. Let’s start by getting the preliminaries, and defining and implementing the ExpensiveCar struct:

#[derive(Clone, Debug)]
struct ExpensiveCar {
    color: String,
}

impl ExpensiveCar {
    fn new(color: String) -> Self {
        ExpensiveCar { color }
    }
}

Next we come to implementing the ExpensiveCarGarage itself:

struct ExpensiveCarGarage<T> where T:Clone {
    cars: Arc<Mutex<Vec<T>>>,
}

impl<T> ExpensiveCarGarage<T> where T:Clone {
    fn new(items: Vec<T>) -> Self {
        ExpensiveCarGarage {
            cars: Arc::new(Mutex::new(items)),
        }
    }

    fn acquire(&self)->Option<PooledObject<T>> {
        let mut items = self.cars.lock().unwrap();
        items.pop().map(|item| PooledObject {
            item,
            pool: Arc::clone(&self.cars),
        })
    }

    fn count(&self) -> usize {
        let items = self.cars.lock().unwrap();
        items.len()
    }
}

A few notes:

  1. We store the cars in a cars field, which is a vector, wrapped in a Mutex which makes sure that only one thread can access it at a time, and an Arc to make sure the garage is safely shareable between threads.
  2. In the acquire() method, we first look the cars mutex, and then map the item if one is available. to a PooledObject which we will define later. Note that if that no object is available, then None is returned.
  3. The count() method is basically a utility method which we will use when testing.

Note that since we use Rust’s Drop-semantics, we do not need to implement a release() method.

Now let’s implement the PooledObject:

#[derive(Debug)]
struct PooledObject<T> where T: Clone {
    item: T,
    pool: Arc<Mutex<Vec<T>>>,
}

impl<T> Drop for PooledObject<T> where T: Clone {
    fn drop(&mut self) {
        println!("Dropping pooled object");
        let mut items = self.pool.lock().unwrap();
        items.push(self.item.clone());
    }
}

impl<T> std::ops::Deref for PooledObject<T> where T:Clone {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        println!("Dereferencing pooled object");
        &self.item
    }
}

impl<T> std::ops::DerefMut for PooledObject<T> where T:Clone {
    fn deref_mut(&mut self) -> &mut Self::Target {
        println!("Dereferencing pooled object mutably");
        &mut self.item
    }
}

A few notes:

  1. We implement the Drop trait, which pushes the item back onto the cars vector. We need to clone this, since after all, the item is about to dropped.
  2. The Deref and DerefMut implementation make sure that the PooledObject can be treated as the object it is wrapping. As you can both the deref() and deref_mut() return an object of type T and not a PooledObject

Testing time

Now we can test our set-up:

fn main() {

    let pool = ExpensiveCarGarage::new(vec![ExpensiveCar::new("red".to_string()), ExpensiveCar::new("blue".to_string())]);
    println!("Pool size: {}", pool.count());
    {
        let mut item = pool.acquire();
        match item {
            Some(ref mut item) => {
                println!("{:?}", item);
                item.color = "transparent".to_string();
            }
            None => println!("No item available"),
        }
        println!("{:?}", item);

    }
    println!("Pool size: {}", pool.count());
    let car=pool.acquire();
    match car {
        Some(ref car) => println!("{:?}", car),
        None => println!("No item available"),
    }
    println!("Pool size: {}", pool.count());
    let last_car=pool.acquire();
    match last_car {
        Some(ref car) => println!("{:?}", last_car),
        None => println!("No item available"),
    }

    let ultimate_car=pool.acquire();
    match ultimate_car {
        Some(ref car) => println!("{:?}", ultimate_car),
        None => println!("No item available"),
    }
    println!("Pool size: {}", pool.count());
}

Step by step:

  1. We create a pool with two cars.
  2. Next we create a scope where we acquire a car, print it out, and after the scope has ended this object is dropped and automatically returned to the pool.
  3. We do the same thing, as you will see, since no pooled object is dropped we can empty the pool as can be seen after the last call to acquire()

Another note: do not use unwrap() or similar constructs unless you are 99% sure nothing can go wrong. Rust’s error-handling is not very difficult to understand, and using it makes your applications stabler and more secure.

Conclusion

Even though the code looks pretty simple, it took a while to get it this simple. However, The fact that you can use Drop, Deref and DerefMut make the implementation easy to understand. For the users of this object pool, these traits makes using this pool quite intuitive, treating PooledObject structs as the object they are wrapping.

Leave a Reply

Your email address will not be published. Required fields are marked *