A self-contained Pool in C++14

published at 22.05.2017 17:20 by Jens Weller

During C++Now I started writing a small application, that plays around with dlibs face recognition features. More on this later, the program uses the QThreadPool, and some researched showed that calling dlib::get_frontal_face_detector() is a very expensive operation. So I decided to write a thread safe pool to share the face detection object between threads, only loading as many as needed. The main thread owns the pool which owns the detection objects.

unique_ptr instead of shared_ptr

On a first thought, shared_ptr seems a good way to implement such a pool. All objects get allocated by the pool, and the clients just receive their copy of a shared pointer which they can use. But unique_ptr guarantees are stronger, and hence I think a bit better when implementing such a pool. Also, with having the option of a custom deleter, one can easily "share" unique pointers. Which is not always a good idea, but in this case, it allows for implementing a self-contained pool, which only hands out objects which call the free method on destruction:

class FrontalFaceDetectorPool
{
    std::istringstream in;
    using del = std::function<void(dlib::frontal_face_detector* ffd)>;
    using rt_ffd = std::unique_ptr<dlib::frontal_face_detector,del>;
    using unique_ffd = std::unique_ptr<dlib::frontal_face_detector>;
    using container = std::vector<unique_ffd>;
    container detectors;
    container::iterator free=detectors.end();
    std::mutex m;
    void freeFacedetector(const dlib::frontal_face_detector* ffd);
public:
    FrontalFaceDetectorPool();
    rt_ffd getFacedetector();
};

The constructor and istringstream are details needed for the correct loading of a dlib face detector, the pool it self does not need them. And refactoring this into a template is planned, for now its a good example for an implementation. The client receives a reference to the pool instance, and then can only query pool objects which free them selves upon destruction. No public free method needed. A mutex ensures that the access to the pool and its free iterator is always synchronized.

Details

This method is called when an instance is queried from the pool:

FrontalFaceDetectorPool::rt_ffd FrontalFaceDetectorPool::getFacedetector()
{
    std::lock_guard<std::mutex> lg(m);
    auto deleter = [this](dlib::frontal_face_detector* ffd){freeFacedetector(ffd);};
    if(free == detectors.end())
    {
        detectors.emplace_back(std::make_unique<dlib::frontal_face_detector>());
        auto ffd = detectors.rbegin()->get();
        dlib::deserialize(*ffd,in);
        in.seekg(0);
        free = detectors.end();
        return rt_ffd{ffd,deleter};
    }
    else
    {
        auto p = free->get();
        free++;
        return rt_ffd{p,deleter};
    }
}

This pool has only one vector in memory, which holds used and currently free instances. The free iterator marks the beginning of the currently not in use objects. If none available, a new one is constructed, from the istringstream containing the basic data needed to be serialized.

The free method only needs to ensure that the returning pointer becomes part of the freelist. The free iterator needs to be decremented by one, and if this isn't the object to be part of the freelist, a simple iterswap makes it happen:

void FrontalFaceDetectorPool::freeFacedetector(const dlib::frontal_face_detector *ffd)
{
    std::lock_guard<std::mutex> lg(m);
    auto it = std::find_if(detectors.begin(),free,[ffd](const unique_ffd& uffd){return uffd.get() == ffd;});
    if(it != detectors.end() && free != detectors.begin() && it != --free)
        std::iter_swap(it,free);
}

Actually this code could be moved into the lambda, which is the deleter for this pool anyways. And of course this should be refactored into a template, which I'll do once I need a second pool for other objects...