Object Pool Design Pattern In C++ - C++ Programming Tutorial
C++ Course / Design Patterns / Object Pool Design Pattern In C++

Object Pool Design Pattern In C++

BLUF: Mastering Object Pool Design Pattern In C++ is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: Object Pool Design Pattern In C++

C++ is renowned for its efficiency. Learn how Object Pool Design Pattern In C++ enables low-level control and high-performance computing in the tutorial below.

Introduction

The Object Pool design pattern falls under the category of creational design patterns, focusing on efficiently reusing costly objects within a system. Its primary goal is to enhance application performance and optimize memory usage by maintaining a pool of pre-initialized objects instead of repeatedly creating and destroying them. When object creation involves significant resource allocation or time limitations, leveraging the Object Pool pattern can greatly enhance the efficiency of an application.

Managing resources within C++ applications can pose significant challenges, particularly when dealing with extensive network connections or objects. Implementing the Object Pool design pattern can effectively reduce expenses, optimize performance, and ensure proper resource management.

Problem Statement

Every system contains categories of entities that are expensive to instantiate and set up. A prime example of this scenario includes database connections or file system handles that require a significant amount of time to establish. Making thoughtful decisions when creating and disposing of these entities can lead to a higher CPU share, more efficient utilization of memory, and prevent detrimental impacts on system performance. Furthermore, in situations where there is a high volume of transactions, the continuous allocation and deallocation of resources can contribute to fragmentation and memory leaks.

Let's consider a scenario: Imagine you're working on a game or software that relies heavily on entities such as projectiles, adversaries, or database links. Generating a fresh entity each time and discarding it once done can result in inefficiency. Hence, there's a necessity for a method to repurpose unused entities, enhancing both memory usage and overall performance.

As an illustration, let's say you're developing a game or an application that includes elements such as projectiles, adversaries, or a link to a database. Instead of generating a new instance of an object whenever necessary and discarding it when it's no longer needed, which is inefficient, it's more effective to implement a system that enables the removal of unused objects. This way, only the memory essential for ongoing operations remains allocated.

Solution: The Object Pool Pattern

The Object Pool design pattern necessitates a significant quantity of objects, which may not be practical. To tackle this issue, the Object Pool design pattern can be employed. This pattern manages a collection of predefined reusable objects known as a "pool." Whenever an object is required, it is retrieved from this pool instead of creating a new one. Once the object is no longer in use, it is returned to the pool for future reuse rather than being disposed of, which would be inefficient. This approach optimizes resource utilization and reduces the overhead associated with creating and destroying resources.

How does it Work?

  • An Object Pool: A collection of a limited number of objects that can be reused.
  • Acquire an Object: This is a procedure within the system when objects are borrowed from the pool in order to perform various functions.
  • Release an Object: This is the step in which objects are returned to the pool after their intended usage.
  • Pool Management: Classically, the pool handles the life cycle of the objects and regulates the number of the objects in circulation.

In C++, effectively utilizing this design pattern involves precise handling of memory and resources to ensure proper initialization of objects upon acquisition and appropriate cleanup upon release.

Implementation of Object Pool in C++

Here is a basic C++ illustration showcasing the Object Pool design pattern. Suppose we have a DatabaseConnection class representing a resource-intensive object to instantiate. The object pool will be employed to oversee and recycle DatabaseConnection objects efficiently.

Step 1: Create the Resource Class

Initially, a class named DatabaseConnection is established to symbolize a connection to a database. Within this class, there are functions to establish a connection and terminate a connection with the database.

Example

#include <iostream>

class DatabaseConnection {
public:
    DatabaseConnection() {
        // Simulate expensive connection setup
        std::cout << "Connecting to database..." << std::endl;
    }

    ~DatabaseConnection() {
        // Simulate cleanup
        std::cout << "Disconnecting from database..." << std::endl;
    }

    void query(const std::string& sql) {
        std::cout << "Executing query: " << sql << std::endl;
    }
};

Step 2: Create the Object Pool Class

Now, we proceed with the creation of the DatabaseConnectionPool class. This particular class is responsible for overseeing a set quantity of DatabaseConnection objects and granting clients the ability to obtain and free up these objects as needed.

Example

#include <vector>
#include <memory>

class DatabaseConnectionPool {
private:
    std::vector<std::unique_ptr<DatabaseConnection>> availableConnections;
    std::vector<std::unique_ptr<DatabaseConnection>> usedConnections;

public:
    DatabaseConnectionPool(size_t poolSize) {
        for (size_t i = 0; i < poolSize; ++i) {
            availableConnections.push_back(std::make_unique<DatabaseConnection>());
        }
    }

    DatabaseConnection* acquireConnection() {
        if (availableConnections.empty()) {
            std::cout << "No available connections!" << std::endl;
            return nullptr;
        }

        auto connection = std::move(availableConnections.back());
        availableConnections.pop_back();
        usedConnections.push_back(std::move(connection));

        return usedConnections.back().get();
    }

    void releaseConnection(DatabaseConnection* connection) {
        auto it = std::find_if(usedConnections.begin(), usedConnections.end(),
                               [connection](const std::unique_ptr<DatabaseConnection>& ptr) {
                                   return ptr.get() == connection;
                               });

        if (it != usedConnections.end()) {
            availableConnections.push_back(std::move(*it));
            usedConnections.erase(it);
        }
    }
};

Step 3: Using the Object Pool

Here is how we can utilize the DatabaseConnectionPool to oversee connections.

Example

int main() {
    DatabaseConnectionPool pool(3);  // Create a pool with 3 connections

    DatabaseConnection* conn1 = pool.acquireConnection();
    if (conn1) conn1->query("SELECT * FROM users");

    DatabaseConnection* conn2 = pool.acquireConnection();
    if (conn2) conn2->query("INSERT INTO users (name) VALUES ('John')");

    pool.releaseConnection(conn1);
    pool.releaseConnection(conn2);

    // Acquiring again after release
    DatabaseConnection* conn3 = pool.acquireConnection();
    if (conn3) conn3->query("DELETE FROM users WHERE name = 'John'");

    pool.releaseConnection(conn3);

    return 0;
}

Program 1:

Example

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <string>

// C++11 version of make_unique
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// The resource class that will be pooled
class DatabaseConnection {
public:
    DatabaseConnection() {
        // Simulate expensive connection setup
        std::cout << "Connecting to database..." << std::endl;
    }

    ~DatabaseConnection() {
        // Simulate cleanup
        std::cout << "Disconnecting from database..." << std::endl;
    }

    void query(const std::string& sql) {
        std::cout << "Executing query: " << sql << std::endl;
    }
};

// The Object Pool class that manages DatabaseConnection objects
class DatabaseConnectionPool {
private:
    std::vector<std::unique_ptr<DatabaseConnection>> availableConnections;
    std::vector<std::unique_ptr<DatabaseConnection>> usedConnections;

public:
    // Constructor to initialize the pool with a specific number of connections
    DatabaseConnectionPool(size_t poolSize) {
        for (size_t i = 0; i < poolSize; ++i) {
            availableConnections.push_back(make_unique<DatabaseConnection>());
        }
    }

    // Method to acquire a connection from the pool
    DatabaseConnection* acquireConnection() {
        if (availableConnections.empty()) {
            std::cout << "No available connections!" << std::endl;
            return nullptr;
        }

        auto connection = std::move(availableConnections.back());
        availableConnections.pop_back();
        usedConnections.push_back(std::move(connection));

        return usedConnections.back().get();
    }

    // Method to release a connection back to the pool
    void releaseConnection(DatabaseConnection* connection) {
        auto it = std::find_if(usedConnections.begin(), usedConnections.end(),
                               [connection](const std::unique_ptr<DatabaseConnection>& ptr) {
                                   return ptr.get() == connection;
                               });

        if (it != usedConnections.end()) {
            availableConnections.push_back(std::move(*it));
            usedConnections.erase(it);
        }
    }
};

int main() {
    DatabaseConnectionPool pool(3);  // Create a pool with 3 connections

    // Acquire and use a connection from the pool
    DatabaseConnection* conn1 = pool.acquireConnection();
    if (conn1) conn1->query("SELECT * FROM users");

    DatabaseConnection* conn2 = pool.acquireConnection();
    if (conn2) conn2->query("INSERT INTO users (name) VALUES ('John')");

    // Release connections back to the pool
    pool.releaseConnection(conn1);
    pool.releaseConnection(conn2);

    // Acquire a connection again, reusing one of the released connections
    DatabaseConnection* conn3 = pool.acquireConnection();
    if (conn3) conn3->query("DELETE FROM users WHERE name = 'John'");

    // Release the last connection
    pool.releaseConnection(conn3);

    return 0;
}

Output:

Output

Connecting to database...
Connecting to database...
Connecting to database...
Executing query: SELECT * FROM users
Executing query: INSERT INTO users (name) VALUES ('John')
Executing query: DELETE FROM users WHERE name = 'John'
Disconnecting from database...
Disconnecting from database...
Disconnecting from database...

Explanation of the Code:

  • DatabaseConnection Class: Represents a connection with a constructor simulating an expensive setup.
  • DatabaseConnectionPool Class: acquireConnection: Acquires a connection from the available pool if possible. releaseConnection: Returns the connection back to the pool when it's no longer needed.
  • Main Function: It demonstrates acquiring, using, and releasing connections from the pool.
  • acquireConnection: Acquires a connection from the available pool if possible.
  • releaseConnection: Returns the connection back to the pool when it's no longer needed.
  • Program 2:

Example

#include <iostream>
#include <vector>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <random>
#include <chrono>
#include <algorithm> // Needed for std::find_if

// C++11 version of make_unique
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// A simulated "Thread" class that performs some work
class Thread {
public:
    Thread(int id) : threadID(id), active(false) {
        std::cout << "Thread " << threadID << " created.\n";
    }

    ~Thread() {
        std::cout << "Thread " << threadID << " destroyed.\n";
    }

    void performTask() {
        std::cout << "Thread " << threadID << " performing task.\n";
        active = true;
        
        // Simulate variable work time
        std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 1000 + 500));
        
        std::cout << "Thread " << threadID << " completed task.\n";
        active = false;
    }

    bool isActive() const {
        return active;
    }

    int getID() const {
        return threadID;
    }

private:
    int threadID;
    bool active;
};

// Thread-safe Object Pool for Thread objects
class ObjectPool {
public:
    ObjectPool(size_t poolSize) {
        for (size_t i = 0; i < poolSize; ++i) {
            pool.push(make_unique<Thread>(i + 1));
        }
    }

    // Acquire a Thread from the pool (blocks if no threads are available)
    std::unique_ptr<Thread> acquire() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return !pool.empty(); });
        
        std::unique_ptr<Thread> thread = std::move(pool.front());
        pool.pop();
        
        return thread;
    }

    // Release a Thread back to the pool
    void release(std::unique_ptr<Thread> thread) {
        std::unique_lock<std::mutex> lock(mtx);
        
        pool.push(std::move(thread));
        cv.notify_one(); // Notify one waiting thread
    }

private:
    std::queue<std::unique_ptr<Thread>> pool;
    std::mutex mtx;
    std::condition_variable cv;
};

// Simulates a client requesting threads from the pool and performing tasks
void clientTask(ObjectPool& pool, int clientID) {
    auto thread = pool.acquire();

    std::cout << "Client " << clientID << " acquired Thread " << thread->getID() << ".\n";
    thread->performTask();

    std::cout << "Client " << clientID << " releasing Thread " << thread->getID() << ".\n";
    pool.release(std::move(thread));
}

int main() {
    const size_t poolSize = 3;
    const int numClients = 5;
    ObjectPool threadPool(poolSize);
    
    std::vector<std::thread> clients;
    
    for (int i = 1; i <= numClients; ++i) {
        clients.emplace_back(clientTask, std::ref(threadPool), i);
    }

    for (auto& client : clients) {
        client.join();
    }

    return 0;
}

Output:

Output

Thread 1 created.
Thread 2 created.
Thread 3 created.
Client 2 acquired Thread 1.
Thread 1 performing task.
Client 3 acquired Thread 2.
Thread 2 performing task.
Client 4 acquired Thread 3.
Thread 3 performing task.
Thread 1 completed task.
Client 2 releasing Thread 1.
Client 5 acquired Thread 1.
Thread 1 performing task.
Thread 3 completed task.
Client 4 releasing Thread 3.
Client 1 acquired Thread 3.
Thread 3 performing task.
Thread 2 completed task.
Client 3 releasing Thread 2.
Thread 1 completed task.
Client 5 releasing Thread 1.
Thread 3 completed task.
Client 1 releasing Thread 3.
Thread 2 destroyed.
Thread 1 destroyed.
Thread 3 destroyed.

Explanation of the Code

  • Thread Class: This class represents a simulated thread that performs some work. Each Thread object has an ID and an active status to indicate if it's currently in use. performTask: Simulates a task by sleeping for a random period (500 to 1500 milliseconds) to mimic real-world processing.
  • ObjectPool Class: This class manages a pool of Thread objects. The pool is initialized with a set number of Thread objects. acquire: Acquires a thread from the pool. If no threads are available, it blocks until one is released back. release: Releases a thread back to the pool and notifies waiting threads that a thread is available. Mutexes (std::mutex and std::condition_variable) ensure thread-safe access to the pool in a concurrent environment.
  • clientTask function : It represents a client's request for a thread from the pool. Each client: Acquires a thread, performs a task, and releases the thread.
  • Main Function: It creates a threadPool with a fixed size (e.g., 3 threads) and multiple clients (e.g., 5 clients). Each client runs in a separate thread, acquiring, using, and releasing threads from the pool.
  • performTask: Simulates a task by sleeping for a random period (500 to 1500 milliseconds) to mimic real-world processing.
  • The pool is initialized with a set number of Thread objects.
  • acquire: Acquires a thread from the pool. If no threads are available, it blocks until one is released back.
  • release: Releases a thread back to the pool and notifies waiting threads that a thread is available.
  • Mutexes (std::mutex and std::condition_variable) ensure thread-safe access to the pool in a concurrent environment.
  • Each client: Acquires a thread, performs a task, and releases the thread.
  • Advantages of the Object Pool Pattern:

Several advantages of the object pool design pattern are as follows:

  • Improved Performance: Objects are reused rather than recreated, which reduces processing overhead.
  • Efficient Resource Management: The pool controls the number of active objects, preventing resource exhaustion.
  • Reduced Memory Fragmentation: By reusing objects, memory allocations and deallocations are minimized.
  • Disadvantages of the Object Pool Pattern

Several disadvantages of the object pool design pattern are as follows:

  • Increased Complexity: Managing object pooling logic adds complexity to the code.
  • Memory Overhead: The pool holds objects in memory even when they are not in use, which can increase memory consumption.
  • Potential Resource Leakage: If objects are not correctly released back to the pool, it could lead to resource leaks.
  • Conclusion:

In summary, the Object Pool design pattern proves to be a valuable asset for effectively managing costly objects in C++. Through resource recycling, it boosts efficiency, optimizes memory utilization, and minimizes the impact of frequent object instantiation and deletion. Nonetheless, a meticulous approach is essential to guarantee correct memory handling and avoid resource leakage. The Object Pool design pattern is particularly advantageous for applications dealing with constrained resources such as database connections or sizable objects, and it delivers significant benefits in high-performance or real-time environments.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience