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:
- 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 anArc
to make sure the garage is safely shareable between threads. - In the
acquire()
method, we first look thecars
mutex, and then map the item if one is available. to aPooledObject
which we will define later. Note that if that no object is available, thenNone
is returned. - 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:
- 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. - The
Deref
andDerefMut
implementation make sure that thePooledObject
can be treated as the object it is wrapping. As you can both thederef()
andderef_mut()
return an object of typeT
and not aPooledObject
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:
- We create a pool with two cars.
- 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.
- 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.