This is a modern C++ implementation of a thread-safe memory pool -- I want to make sure I am solving the critical section problem correctly (no deadlocks, starvation, bounded waiting) and I am getting details like the rule of five correct (copy not allowed, move doesn't trigger double release). Design criteria:
- Small number of items in pool (e.g. 5 to 10) since each may have a large memory footprint -- the number can be decided / fixed when the pool is created.
- Uses RAII to guarantee an object is released when code leaves scope.
- If there are no items available in the pool, the code blocks until one is available.
The typical usage pattern would be:
ObjectPool<Foo> pool(5, Foo_ctor_args);
...
{
ObjectPool<Foo>::Item foo = pool.acquire()
foo.object.doSomething();
}
Class template:
#include <vector>
#include <mutex>
#include <condition_variable>
#include <cassert>
#include <iostream>
template <typename T>
class ObjectPool {
private:
std::vector<T> objects;
std::vector<bool> inUse;
std::mutex mutex;
std::condition_variable cond;
public:
class Item {
public:
T& object;
Item(T& o, size_t i, ObjectPool& p) : object{o}, index{i}, pool{p} {}
Item(const Item&) = delete;
Item(Item&& other) : object{other.object}, index{other.index}, pool{other.pool} {
other.index = bogusIndex; // <-- don't release
}
Item& operator=(const Item&) = delete;
Item& operator=(Item&& other) {
if (this != &other) {
object = other.object;
index = other.index;
pool = other.pool;
other.index = bogusIndex; // <-- don't release
}
return *this;
}
~Item() {
if (index != bogusIndex) {
pool.release(index);
index = bogusIndex; // <-- avoid double release
}
}
private:
constexpr static size_t bogusIndex = 65535;
size_t index;
ObjectPool<T>& pool;
};
template<typename... Args>
ObjectPool(size_t maxElems, Args&&... args) : inUse(maxElems, false) {
for (size_t i = 0; i < maxElems; i++)
objects.emplace_back(std::forward<Args>(args)...);
}
Item acquire() {
std::unique_lock<std::mutex> guard(mutex);
while (true) {
for (size_t i = 0; i < objects.size(); i++)
if (!inUse[i]) {
inUse[i] = true;
return Item{objects[i], i, *this};
}
cond.wait(guard);
}
}
private:
void release(size_t index) {
std::unique_lock<std::mutex> guard(mutex);
assert(index < objects.size());
assert(inUse[index]);
inUse[index] = false;
cond.notify_all();
}
};
std::size_t
instead ofsize_t
. Becausesize_t
is implementation defined and can cause problems on different compilers. \$\endgroup\$