3 releases (breaking)

0.3.0 Feb 16, 2021
0.2.0 Aug 17, 2019
0.1.0 Aug 17, 2019

#1222 in Data structures

MIT/Apache

23KB
434 lines

swimmer

Thread-safe object pools for Rust.

use swimmer::Pool;

let pool: Pool<String> = Pool::with_size(10);
assert_eq!(pool.size(), 10);

let value = pool.get()
assert_eq!(pool.size(), 9);
assert_eq!(*value, "");

drop(value);
// Value is returned to pool
assert_eq!(pool.size(), 10);

See the documentation for more.


lib.rs:

A thread-safe object pool for Rust.

An object pool is used to reuse objects without reallocating them. When an object is requested from a pool, it is taken out of the pool; once it is dropped, it is returned to the pool and can be retrieved once more.

The main type of this crate is the Pool struct, which implements a thread-safe object pool. It can pool objects which implement Recyclable, a trait which allows the pool to initialize and "recycle" an object.

The implementation of this is as follows:

  • A pool is created using the builder function. It is configured with an initial size.
  • Upon creation of the pool, the pool initializes initial_size values using Recyclable's new function.
  • When a value is requested from the pool, usually using Pool::get(), a value is taken out of the internal buffer. If there are no remaining values, a new object is initialized using Recyclable::new().
  • The value can then be used by the caller.
  • When the value is dropped, it is returned to the pool, and future calls to Pool::get() may return the same object.

To ensure that the object is cleaned, the pool calls Recyclable::recycle() on the object before returning it to the pool. This function removes any mutated state of the object, effectively "resetting" it. For example, see the following sequence of events:

  • A pool of vectors is initialized.
  • A vector is retrieved from the pool, and some values are added to it.
  • The vector is dropped and returned to the pool.

Without resetting the vector, future calls to Pool::get could return a vector containing those old elements; clearly, this is not desirable. As a result, the Recyclable implementation for Vec clears the vector when recycling.

This crate is heavily based on the lifeguard crate, but it is thread-safe, while lifeguard is not.

Thread safety

Pool is thread-safe, and it can be shared across threads or used in a lazily-initialized static variable (see the examples).

This is currently implemented by making the pool contain a thread-local buffer for each thread, which has been proving by benchmarks to be more than twice as performant as using a locked Vec or crossbeam::SegQueue.

Supplier

In some cases, you may want to specify your own function for initializing new objects rather than use the default Recyclable::new() function. In this case, you can optionally use PoolBuilder::with_supplier(), which will cause the pool to use the provided closure to initialize new values.

For example, the Recyclable implementation for Vec<T> allocates a vector with zero capacity, but you may want to give the vector an initial capacity. In that case, you can do this, for example:

use swimmer::Pool;
let pool: Pool<Vec<u32>> = swimmer::builder()
    .with_supplier(|| Vec::with_capacity(128))
    .build();

let vec = pool.get();
assert_eq!(vec.capacity(), 128);

Note, however, that the supplier function is only called when the object is first initialized: it is not used to recycle the object. This means that there is currently no way to implement custom recycling functionality.

Crate features

  • hashbrown-impls: implements Recyclable for hashbrown::HashMap and hashbrown::HashSet.
  • smallvec-impls: implements Recyclable for SmallVec.

Examples

Basic usage:

use swimmer::Pool;

// Initialize a new pool, allocating
// 10 empty values to start
let pool: Pool<String> = swimmer::builder()
    .with_starting_size(10)
    .build();

assert_eq!(pool.size(), 10);

let mut string = pool.get();
assert_eq!(*string, ""); // Note that you need to dereference the string, since it is stored in a special smart pointer
string.push_str("test"); // Mutate the string

// One object was taken from the pool,
// so its size is now 9
assert_eq!(pool.size(), 9);

// Now, the string is returned to the pool
drop(string);

assert_eq!(pool.size(), 10);

// Get another string from the pool. This string
// could be the same one retrieved above, but
// since the string is cleared before returning
// into the pool, it is now empty. However, it
// retains any capacity which was allocated,
// which prevents additional allocations
// from occurring.
let another_string = pool.get();
assert_eq!(*another_string, "");

Implementing Recyclable on your own object:

use swimmer::{Pool, Recyclable};

struct Person {
    name: String,
    age: u32,
}

impl Recyclable for Person {
    fn new() -> Self {
        Self {
            name: String::new(),
            age: 0,
        }
    }

    fn recycle(&mut self) {
        // You are responsible for ensuring
        // that modified `Person`s get reset
        // before being returned to the pool.
        // Otherwise, the object could be put
        // back into the pool with its old state
        // still intact; this could cause weird behavior!
        self.name.clear();
        self.age = 0;
     }
}

let pool: Pool<Person> = Pool::new();
let mut josh = pool.get();
josh.name.push_str("Josh"); // Since `recycle` empties the string, this will effectively set `name` to `Josh`
josh.age = 47;

drop(josh); // Josh is returned to the pool and his name and age are reset

// Now get a new person
let another_person = pool.get();

Using a Pool object in a lazy_static variable, allowing it to be used globally:

use lazy_static::lazy_static;
use swimmer::Pool;

lazy_static! {
    static ref POOL: Pool<String> = {
        Pool::new()
    };
}

let value = POOL.get();

Dependencies

~43–290KB