一、线程池设计基础
1.1 线程池基础
关于通用线程池的设计轮子,可以参考本人的博客:从0开始实现线程池(C++)
相比上面造的轮子,本文进一步将Task类型设置成模板类,以适应不同类型的任务。同时将worker的阻塞环节中条件变量替换为信号量,以实现对线程池的互斥访问。
1.2 半同步/半反应堆线程池
并发模式中的同步和异步
- 同步:指的是发起一个调用后,调用者需要等待结果,才能继续后续的操作;这种程序完全按照代码的顺序执行
- 异步:指的是发起一个调用后,调用者不需要等待结果,就可以继续后续的操作;异步指的是程序的执行需要由系统事件驱动
半同步/半反应堆线程池是一种同步I/O模拟Proactor事件处理模式,主线程负责处理读写数据、接受新连接等I/O操作,等处理完事件后再直接发送读取后的结果给工作线程进行业务处理。以proactor模式为例,半同步/半反应堆线程池的工作流程如下:
- 主线程中epoll充当异步线程,负责处理socket上读写数据、接受新连接等I/O操作
- 若有新连接发生,主线程获取该新连接,然后往epoll内核事件表注册该socket上的读就绪事件
- 如果连接socket上有数据可读,主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列
- 睡眠的工作线程接收到请求对象后,通过竞争获取任务接管权
本项目除了采取半同步/半反应堆线程池,还对worker中处理任务允许使用Reator模式或Proactor模式,Reactor模式需要输入一个state参数,用于判断任务的类型,而Proactor模式则不需要,因此这里Reactor和Proactor模式封装的添加任务的函数不同,一个需要输入state参数,一个不需要。(但是任务都是模板类型,也就是http_conn
具体类)
本项目的线程池设计和实现流程如下:
1.3 主要函数代码实现
主要函数包括生产者向任务队列中添加任务append
函数、消费者从任务队列中取出任务并执行任务worker
工作线程回调函数。
append
函数:生产者向任务队列中添加任务
1 | //向请求队列中添加任务(Reactor模式) |
worker
函数:消费者从任务队列中取出任务并执行任务
1 | //工作线程运行函数:worker |
二、封装线程同步需要的信号量和各种锁
2.1 信号量
信号量的封装主要是对sem_init
、sem_wait
、sem_post
和sem_destroy
函数的封装,以实现对信号量的操作。同时维护一个sem_t
信号量变量,用于实现对信号量的操作。
1 | /*封装信号量的类 |
2.2 互斥锁
互斥锁的封装主要是对pthread_mutex_init
、pthread_mutex_lock
、pthread_mutex_unlock
和pthread_mutex_destroy
函数的封装,以实现对互斥锁的操作。同时维护一个pthread_mutex_t
互斥锁变量,用于实现对互斥锁的操作。
1 | /*封装互斥锁的类*/ |
2.3 条件变量
条件变量的封装主要是对pthread_cond_init
、pthread_cond_wait
、pthread_cond_timedwait
、pthread_cond_signal
和pthread_cond_broadcast
函数的封装,以实现对条件变量的操作。同时维护一个pthread_cond_t
条件变量变量,用于实现对条件变量的操作。
1 | /*封装条件变量的类*/ |
三、数据库连接池设计
数据库连接池实际上也是线程池中维护的一个共享资源,每接收一个http请求时除了分配线程资源外,当获得线程资源后,线程池的worker中还需要分配数据库连接资源。
连接池的功能主要有:初始化,获取连接、释放连接,销毁连接池。
3.1 连接池中信号量的使用
为了维护数据库连接池,这里将条件变量替换为信号量,以实现对连接池的互斥访问。(条件变量是用于线程间的同步,而信号量可以用于进程间或线程间的同步)
在数据库连接池中,信号量(Semaphore)通常用于控制对连接资源的并发访问。连接池是一种重复使用数据库连接的技术,通过维护一组预先创建的连接对象,在需要访问数据库时从池中获取连接,使用完毕后将连接放回池中,以提高数据库访问的性能和效率。
在C++中,信号量可以使用sem_init
、sem_wait
、sem_post
和sem_destroy
等函数进行操作:
int sem_init(sem_t *sem, int pshared, unsigned int value)
:初始化信号量- sem:指向要初始化的信号量的指针。
- pshared:指定信号量的共享类型。如果为
0
,信号量将被进程内的线程共享;如果为非零,信号量可以在不同进程间共享。 - value:指定信号量的初始值。
int sem_wait(sem_t *sem)
:等待信号量,用于获取一个信号量的许可证,没有可用许可证时,线程将阻塞。- sem:指向要等待的信号量的指针。
int sem_post(sem_t *sem)
:释放信号量,用于释放一个信号量的许可证。- sem:指向要释放的信号量的指针。
- 如果有其他线程在等待这个信号量,那么其中的一个线程将被唤醒。
- 释放信号量的许可证后,信号量的值将增加1。
int sem_destroy(sem_t *sem)
:销毁信号量。- sem:指向要销毁的信号量的指针。
使用信号量实现多线程争夺连接的同步机制,这里将信号量初始化为数据库的连接总数。当线程需要获取连接时,取出连接它会调用sem_wait
函数,如果有可用的连接,线程将获得一个连接并将信号量的值减1;如果没有可用的连接,线程将阻塞等待,直到有连接被释放。当线程使用完连接后,释放连接会调用sem_post
函数将信号量的值加1。
另外,由于多线程操作连接池,会造成竞争,这里使用互斥锁完成同步,具体的同步机制均使用lock.h
中封装好的类。这里互斥锁的使用在一、中已有说明,不再赘述。
3.2 数据库连接池的单例模式
数据库连接池是一个全局的资源,因此需要使用单例模式来保证程序中只有一个数据库连接池的实例。这里采用懒汉模式实现单例模式,由于项目中在程序启动(main.cpp
)时就会初始化数据库连接池,因此不需要考虑多线程安全问题,采用最简单的懒汉模式实现单例模式。(24.4.11更正:这里其实是线程安全的,C++11后局部静态变量的初始化是线程安全的)
1 | class connection_pool |
3.3 RAII机制封装数据库的连接与释放
RAII(Resource Acquisition Is Initialization)是C++的一种资源管理方式,即资源获取就是初始化。RAII的核心思想是将资源的生命周期与对象的生命周期绑定,通过对象的生命周期来管理资源的生命周期。
在获取连接时,通过实例化一个connectionRAII
对象会自动调用mysql_real_connect
中的GetConnection
函数从而获取一个mysql连接;
在释放连接时,通过销毁上面的connectionRAII
实例化对象,自动调用析构函数执行RealeaseConnection
函数对连接进行释放。
1 | /*RAII机制,用于自动释放和获取数据库连接(从连接池中获取和释放连接) |
1 | /*RAII机制,用于自动释放和获取数据库连接*/ |
- 从本项目中举例说明RAII机制的使用
在http_conn.cpp
中,我们通过一个initmysql_result
函数在整个程序初始化时就先取出当前所有注册过的用户名和密码到一个map中存起来
我们可以看到在这个函数中我们并没有显示地连接和释放数据库连接,那么它是怎么实现数据库资源的获取和释放的呢?就是用的我们封装好的RAII机制函数。
此时函数中就是通过实例化一个connectionRAII
对象,这个对象是函数内的局部变量,所以当这个函数结束时,这个对象就会被销毁,从而调用析构函数,自动释放本次的数据库连接(归还给数据库连接池)。
1 | //main中初始化WebServer类中的m_connPool时会同时在HTTP类中取出一个数据库连接用于提前将所有注册过的用户信息取出存在map中 |
四、总结
本文主要介绍了线程池的设计和数据库连接池的设计,其中线程池的设计主要是对通用线程池的进一步封装,将Task类型设置成模板类,以适应不同类型的任务。同时将worker的阻塞环节中条件变量替换为信号量,以实现对线程池的互斥访问。
数据库连接池的设计主要是对连接池中信号量的使用和数据库连接池的单例模式的设计。同时,使用RAII机制封装数据库的连接与释放,避免手动释放。
通过本文的学习,对线程池和数据库连接池的设计有了更深入的了解,同时也对C++的RAII机制有了更深入的认识。有了本文的基础,我们可以开始学习HTTP类。关于HTTP类的实现,可以参考本人的下一篇博客:WebServer学习6:HTTP连接处理及报文机制