基础
对象池模式(Object Pool Pattern)是一种设计模式,旨在管理对象的重用,以减少频繁创建和销毁对象的开销。在需要创建和销毁大量相似对象时,使用对象池可以提高性能,尤其是在性能敏感的场合,比如游戏开发或高频交易系统。
对象池模式的基本概念
- 池(Pool):存储可复用对象的集合。
- 借用(Borrowing):从池中获取一个对象。
- 归还(Returning):使用完后将对象放回池中,以便重用。
使用现代 C++ 实现对象池
第一步:定义对象
我们先定义一个简单的对象类 Object
。
#include <iostream>class Object {
public:Object() {std::cout << "Object Created" << std::endl;}~Object() {std::cout << "Object Destroyed" << std::endl;}void doSomething() {std::cout << "Doing something with object!" << std::endl;}
};
第二步:实现对象池
接下来,我们实现一个对象池类 ObjectPool
。
#include <vector>
#include <memory>
#include <iostream>class ObjectPool {
public:ObjectPool(size_t initialSize) {for (size_t i = 0; i < initialSize; ++i) {pool.push_back(std::make_unique<Object>());}}std::unique_ptr<Object> borrowObject() {if (pool.empty()) {std::cout << "No available objects, creating a new one." << std::endl;return std::make_unique<Object>();} else {auto obj = std::move(pool.back());pool.pop_back();return obj;}}void returnObject(std::unique_ptr<Object> obj) {pool.push_back(std::move(obj));}private:std::vector<std::unique_ptr<Object>> pool;
};
第三步:使用对象池
现在,我们可以在 main
函数中使用对象池。
int main() {ObjectPool pool(3); // 初始化对象池,预先创建3个对象// 借用对象auto obj1 = pool.borrowObject();obj1->doSomething();// 归还对象pool.returnObject(std::move(obj1));// 再次借用对象auto obj2 = pool.borrowObject();obj2->doSomething();// 归还对象pool.returnObject(std::move(obj2));// 借用更多对象auto obj3 = pool.borrowObject();auto obj4 = pool.borrowObject();auto obj5 = pool.borrowObject();auto obj6 = pool.borrowObject(); // 这里会创建一个新对象return 0;
}
对象池模式特别适合于那些频繁创建和销毁对象的场景。
More example
模拟一个数据库连接池,用于管理多个数据库连接。这样,我们可以避免频繁地打开和关闭连接,从而提升系统的效率。
复杂例子:数据库连接池
第一步:定义数据库连接类
我们定义一个模拟的 DatabaseConnection
类,用于表示一个数据库连接。
#include <iostream>
#include <string>
#include <thread>
#include <chrono>class DatabaseConnection {
public:// 构造函数:接受连接字符串并连接数据库DatabaseConnection(const std::string& connectionStr) : connectionStr_(connectionStr), connected_(false) {connect();}// 析构函数:断开连接~DatabaseConnection() {disconnect();}// 模拟连接数据库的过程void connect() {if (!connected_) {std::cout << "Connecting to database: " << connectionStr_ << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟连接延迟connected_ = true;}}// 断开数据库连接void disconnect() {if (connected_) {std::cout << "Disconnecting from database: " << connectionStr_ << std::endl;connected_ = false;}}// 模拟执行数据库查询void query(const std::string& sql) {if (connected_) {std::cout << "Executing query: " << sql << " on " << connectionStr_ << std::endl;} else {std::cerr << "Database not connected!" << std::endl;}}private:std::string connectionStr_; // 数据库连接字符串bool connected_; // 连接状态标志
};
第二步:实现数据库连接池
创建一个 DatabaseConnectionPool
类,使用对象池管理数据库连接。这里使用 std::shared_ptr
来实现连接的共享和重用。
#include <vector>
#include <memory>
#include <mutex>
#include <string>
#include <queue>
#include <condition_variable>class DatabaseConnectionPool {
public:// 构造函数:初始化连接池并创建一定数量的数据库连接DatabaseConnectionPool(const std::string& connectionStr, size_t poolSize): connectionStr_(connectionStr), poolSize_(poolSize) {// 创建指定数量的连接并存入池中for (size_t i = 0; i < poolSize_; ++i) {pool_.push(std::make_shared<DatabaseConnection>(connectionStr_));}}// 获取一个连接,如果池中没有可用连接则阻塞等待std::shared_ptr<DatabaseConnection> acquireConnection() {//互斥锁确保多线程访问时的线程安全std::unique_lock<std::mutex> lock(mutex_);// 条件变量等待,若池中无可用连接,阻塞当前线程并等待直到有连接归还到池中。cond_.wait(lock, [this]() { return !pool_.empty(); });// 从池中取出一个连接auto connection = pool_.front();pool_.pop();// 使用自定义删除器,将连接自动归还到池中return std::shared_ptr<DatabaseConnection>(connection.get(), [this](DatabaseConnection* conn) { std::unique_lock<std::mutex> lock(mutex_);// 将连接归还到池中pool_.push(std::shared_ptr<DatabaseConnection>(conn));// 通知其他等待线程有新的连接可用cond_.notify_one(); });}private:std::string connectionStr_; // 数据库连接字符串size_t poolSize_; // 池的大小(预先创建的连接数)std::queue<std::shared_ptr<DatabaseConnection>> pool_; // 存储连接的队列std::mutex mutex_; // 用于线程安全的互斥锁std::condition_variable cond_; // 条件变量,用于管理等待的线程
};
在 acquireConnection
方法中,使用了自定义的删除器,这样当 std::shared_ptr
的引用计数减少到零时,连接会自动归还到池中。
第三步:使用数据库连接池
int main() {// 创建一个数据库连接池,预先创建 3 个数据库连接DatabaseConnectionPool dbPool("Server=127.0.0.1;Database=testdb;", 3);{// 从连接池中借用一个连接auto conn1 = dbPool.acquireConnection();conn1->query("SELECT * FROM users"); // 使用连接执行查询// 借用第二个连接auto conn2 = dbPool.acquireConnection();conn2->query("SELECT * FROM orders"); // 使用连接执行查询// 借用第三个连接auto conn3 = dbPool.acquireConnection();conn3->query("SELECT * FROM products"); // 使用连接执行查询// conn1, conn2, conn3 都在作用域内,因此此时池中没有可用连接// 如果再请求一个连接,当前线程会阻塞,直到有连接被归还} // 离开作用域,conn1, conn2, conn3 自动销毁并归还到连接池中// 作用域结束后,池中有连接可以使用,再次借用连接auto conn4 = dbPool.acquireConnection();conn4->query("SELECT * FROM logs"); // 使用连接执行查询return 0;
}
运行流程
- 创建数据库连接池
dbPool
,并初始化 3 个连接。 acquireConnection
会从池中取出一个连接;如果没有连接,线程会阻塞等待。- 使用完连接后,智能指针销毁时触发自定义删除器,将连接归还到池中,避免显式地
release
。
注意点
- 线程安全:对象池常常用于多线程环境,需要用互斥锁(
std::mutex
)和条件变量(std::condition_variable
)来确保线程安全。 - 连接超时:在实际项目中,可以为
acquireConnection
增加超时参数,如可以使用condition_variable::wait_for
设置超时时间,如果等待时间超过设定值,就返回nullptr
,避免线程长时间阻塞。 - 最大池容量:可以动态调整池的大小或设定池的最大容量,当需求量大于容量时,临时创建新对象,而不是等待。
拓展点
- 惰性初始化:在对象池初始化时不创建对象,而是根据请求动态创建,可以减少启动时的资源占用。
- 双层对象池:为频繁使用的对象(如小对象和大对象)分别创建对象池,从而提高内存和性能管理的精细度。
- 调度策略:可以引入优先级调度,例如优先分配最近未使用的对象,或根据对象的状态和属性进行选择。
总结
对象池模式在现代 C++ 中结合智能指针、条件变量、互斥锁等技术,可以实现高效、易用的资源管理器。在实际应用中,数据库连接池只是一个简单的示例,对象池还可以应用于线程池、网络连接池、文件句柄池等各种场景。
与线程池的关系
对象池和线程池在设计模式上非常相似,它们都通过复用有限资源来减少创建和销毁资源的开销。这两者的核心思想相同:使用池管理资源,避免频繁的分配和释放,但它们应用的场景和管理的资源有所不同。
对象池与线程池的相似之处
- 资源复用:无论是对象池还是线程池,都是为了管理有限的资源池,避免频繁的资源分配和回收。
- 管理资源的生命周期:两者都通过池的机制来统一管理资源的创建、借用、归还、销毁等生命周期,避免内存泄漏或资源浪费。
- 多线程安全:在多线程环境中,两者都需要确保资源的安全访问,一般通过互斥锁(
mutex
)和条件变量(condition_variable
)来管理资源的获取与归还,避免资源竞争。 - 借用和归还机制:两者都通过“借用”和“归还”的方式来管理资源的使用。资源借出后可以在外部使用,用完后再放回池中,以便再次被其他任务使用。
对象池与线程池的不同之处
虽然它们在设计模式上相似,但用途和实现细节有所不同:
-
资源类型:
- 对象池:管理的是一般的对象实例(如数据库连接、文件句柄等),这些对象一般代表一个外部资源或具有较高创建/销毁开销的对象。
- 线程池:管理的是线程或任务执行的资源,主要用于控制线程的数量,并让任务在已有的线程中并行执行,从而避免线程频繁创建和销毁的开销。
-
资源的使用方式:
- 对象池:对象可以是任何类型的数据结构或连接,不仅限于线程。用户借用对象时一般直接调用对象的方法。
- 线程池:线程池内部封装了线程管理逻辑,用户提交任务后,线程池会从任务队列中提取任务并分配给线程执行,用户不直接控制线程对象。
-
任务调度:
- 对象池:对象池通常不会处理任务调度,而是将对象直接提供给用户使用。
- 线程池:线程池具有任务调度的职责,用户向线程池提交任务后,线程池负责管理任务队列、分配任务给空闲线程并调度任务的执行。
示例:简化的线程池实现(对比对象池)
为了更直观地展示线程池和对象池的不同,这里给出一个简单的线程池实现。这个线程池将使用一个任务队列来管理用户提交的任务,并由线程池中的线程从队列中取出任务并执行。
简化的线程池实现代码
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>
#include <atomic>class ThreadPool {
public:ThreadPool(size_t poolSize) : stop(false) {// 创建指定数量的线程for (size_t i = 0; i < poolSize; ++i) {workers.emplace_back([this]() {while (true) {std::function<void()> task;{std::unique_lock<std::mutex> lock(this->queueMutex);// 等待任务,或线程池停止this->condition.wait(lock, [this]() { return this->stop || !this->tasks.empty(); });// 如果线程池停止且任务队列为空,则退出if (this->stop && this->tasks.empty()) {return;}// 从任务队列中取出一个任务task = std::move(this->tasks.front());this->tasks.pop();}// 执行任务task();}});}}// 添加任务到任务队列template <class F, class... Args>auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {using return_type = typename std::result_of<F(Args...)>::type;// 打包任务,使其可以存储到队列中auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<return_type> res = task->get_future();{std::unique_lock<std::mutex> lock(queueMutex);// 停止状态下不允许添加新任务if (stop) {throw std::runtime_error("enqueue on stopped ThreadPool");}// 将任务添加到队列tasks.emplace([task]() { (*task)(); });}// 通知一个等待中的线程去处理任务condition.notify_one();return res;}// 停止线程池~ThreadPool() {{std::unique_lock<std::mutex> lock(queueMutex);stop = true;}// 通知所有线程退出condition.notify_all();// 等待所有线程完成任务并退出for (std::thread &worker : workers) {worker.join();}}private:std::vector<std::thread> workers; // 工作线程集合std::queue<std::function<void()>> tasks; // 任务队列std::mutex queueMutex; // 任务队列的互斥锁std::condition_variable condition; // 条件变量,通知线程有任务可执行std::atomic<bool> stop; // 停止线程池标志
};
使用线程池
int main() {ThreadPool pool(4); // 创建线程池,包含4个线程// 向线程池提交任务,并获取任务的 future 以便同步获取结果auto result1 = pool.enqueue([](int x) { std::this_thread::sleep_for(std::chrono::seconds(1));return x * x; }, 5);auto result2 = pool.enqueue([]() { std::cout << "Task 2 is running" << std::endl; });std::cout << "Result of Task 1: " << result1.get() << std::endl; // 阻塞等待结果result2.get(); // 确保任务2完成return 0;
}
总结
在这个线程池实现中:
- 任务调度:线程池维护一个任务队列并负责调度任务的执行。
- 线程管理:线程池管理了固定数量的线程来执行任务,避免频繁创建和销毁线程。
- 资源复用:线程池中的线程会反复执行多个任务,而不是每个任务都创建新线程。
通过对比可以看出,对象池管理的是一些资源的生命周期,而线程池则管理任务的调度和线程的生命周期。线程池在任务密集型的程序中非常有用,而对象池则适合于高成本、可重用的资源管理。两者都在现代编程中扮演着重要角色,用于提升系统的性能和资源利用效率。