手把手实现web server网页服务器
第二章 实现webserver的线程池
前言须知 线程同步与互斥量
本章节介绍,在webserver实现的过程用,多个客户端与服务器之间的连接采用了线程池的管理,线程同步机制封装类,包含了
一、互斥锁类
二、条件变量类
三、信号量类
----- 在此之上我们还需要对线程同步和互斥量进行了解才可以更加顺利的摸透线程池的管理机制。
线程同步 : 能够通过全局变量共享信息,不过这种便捷的共享 有代价的,必须确保多个线程不会同时修改。
同一个变量,或者某一线程不会读取正在使用的变量。(详细可以查看消费者模型)临界区是某一共享资源的代码段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应中断该片段的执行。
当一个线程堆内存进行操作时刻,其他线程不可以堆这个内存地址进行操作,
直到该线程 也就是临界区的操作结束后,才可以操作,其他线程处于等待状态
互斥量
为了避免线程更新共享变量的问题 可以用互斥量 (mutex == mutual exclusion)
确保了 仅有一个线程可以访问该共享段 。可以使用互斥量来保证资源的原子性;
互斥量有两种状态 locked / unlocked 。
任何时候,至多只有一个线程 可以锁定互斥量,只有所有者才可以解锁。
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
释放
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex : 需要初始化的互斥量
attr : 互斥量相关属性 一般为null
备注:: restrict c语言的修饰符 被修饰的指针不能由另外一个指针操作
int pthread_mutex_lock(加锁操作)
阻塞 如果有一个线程加锁 其他线程只能阻塞等待
int pthread_mutex_unlock( 释放锁)
一、互斥锁类
代码如下(示例):
#include<exception> //异常头文件
#include<pthread.h>
#include<semaphore.h>
class locker
{
public:
locker() {
if(pthread_mutex_init(&my_mutex, NULL) != 0) {
throw std::exception();
}
}
~locker() {
pthread_mutex_destroy(&my_mutex);
}
bool lock(){
return pthread_mutex_lock(&my_mutex)==0;
}
bool unlock(){
return pthread_mutex_unlock(&my_mutex)==0;
}
pthread_mutex_t *get()
{
return &my_mutex;
}
private:
/* data */
pthread_mutex_t my_mutex;
};
二、条件变量类
条件变量同锁一起使用使得线程可以以一种无竞争的方式等待任意条件的发生。所谓无竞争就是,条件改变这个信号会发送到所有等待这个信号的线程。而不是说一个线程接受到这个消息而其它线程就接收不到了。
条件变量 不是锁 实现不了数据不混乱
某个条件满足后,才能解除阻塞 ,配合互斥量使用才可以实现线程同步
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_timedwait( pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);‘
功能 调用函数 线程阻塞 直到指定的时间abstime
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex)
功能 等待阻塞函数, 参数 条件变量cond 和互斥量mutex
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待的线程
int pthread_cond_signal(pthread_cond_t *cond);
唤醒一个或者多个等待的线程
条件变量用法流程
1 创建变量 并且初始化
2 合理使用条件变量的2种wait方式
3 wait处一直等待,直到满足条件后 进行signal通知
4 destroy,在销毁前可以唤醒所有等待的线程。
详细条件变量可以参考这篇博客 讲的很细点我~
代码如下(示例):
class cond
{
public:
cond(/* args */){
if(pthread_cond_init(&my_cond,NULL)!=0)
{
throw std::exception();
}
}
~cond(){
pthread_cond_destroy(&my_cond);
}
bool wait(pthread_mutex_t *mutex){
return pthread_cond_wait(&my_cond,mutex)==0;
}
bool wait(pthread_mutex_t *mutex,struct timespec t){
return pthread_cond_timedwait(&my_cond,mutex,&t)==0;
}
bool signal(pthread_mutex_t *mutex){
return pthread_cond_signal(&my_cond)==0;
}
bool broadcast(){
return pthread_cond_broadcast(&my_cond)==0;
}
private:
/* data */
pthread_cond_t my_cond;
};
三、信号量类
信号量 Semaphore
类似互斥锁,但它可以允许多个线程同时访问一个共享资源
通过使用一个计数器来控制对共享资源的访问,如果计数器大于0,就允许访问,如果等于0,就拒绝访问。计数器累计的是“许可证”的数目,为了访问某个资源。线程必须从信号量获取一个许可证。
通常在使用信号量时,希望访问共享资源的线程将尝试获取一个许可证,如果信号量的计数器大于0,线程将获取一个许可证并将信号量的计数器减1,否则先线程将阻塞,直到获取一个许可证;当线程不再需要共享资源时,将释放锁拥有的许可证,并将许可证的数量加1,如果有其他的线程正在等待许可证,那么该线程将立刻获取许可证。
代码如下(示例):
class sem
{
public:
sem() {
if( sem_init( &m_sem, 0, 0 ) != 0 ) {
throw std::exception();
}
}
sem(int num) {
if( sem_init( &m_sem, 0, num ) != 0 ) {
throw std::exception();
}
}
~sem() {
sem_destroy( &m_sem );
}
bool wait(){
return sem_wait(&m_sem)==0;
}
bool post(){
return sem_post(&m_sem)==0;
}
private:
/* data */
sem_t m_sem;
};
四、线程池的封装
先从整体思考线程池需要用到哪些元素,再去理解线程池中需要用到的成员变量以及成员函数。
封装类
#include<pthread.h>
#include<list>
#include<cstdio>
#include<exception>
#include"my_locker.h"
//找到一个任务去执行
//定义成模板类 方便代码的复用 参数T是任务类
template <typename T>
class my_threadpool
{
public:
my_threadpool(int thread_number=8,int max_requests=10000);
~my_threadpool();
bool append(T* request);
private:
static void* worker(void * arg);
void run();
private:
/* data */
//数量
int m_thread_number;
//线程数组 大小为m_thread_number
pthread_t *m_threads;
//请求队列最多的数量
int m_max_requests;
//请求队列
std::list< T* > m_workqueue;
//保护请求队列的互斥锁
locker my_queuelock;
// 是否有任务需要处理
sem m_queuestat;
// 是否结束线程
bool m_stop;
};
构造与析构
pthread_create(m_threads+i,NULL,worker,this) 调用的工作函数,作为多线程的主体,通过上下文的传递,来访问
template< typename T >
void* my_threadpool< T >::worker( void* arg )
{
//静态的访问不到 成员对象本身 在创建线程的时候把this指针传递过去 通过pool去访问
my_threadpool pool =(my_threadpool)arg;
pool->run();
return pool;
}
template<typename T>
my_threadpool<T>::my_threadpool(int thread_number, int max_requests)
{
m_thread_number = thread_number;
m_max_requests = max_requests;
m_stop = false;
m_threads= NULL;
if((thread_number<=0)||(max_requests<=0)){
throw std::exception();
}
m_threads = new pthread_t[m_thread_number];
if(!m_threads){
throw std::exception();
}
//创建thread_number个线程 并讲他们设置为线程脱离
for (size_t i = 0; i < thread_number; i++)
{
printf("creadt the %dth thread\n",i);
if (pthread_create(m_threads+i,NULL,worker,this)!=0)
{
delete[]m_threads;
throw std::exception();
}
if( pthread_detach( m_threads[i] ) ) {
delete [] m_threads;
throw std::exception();
}
}
}
template<typename T>
my_threadpool<T>::~my_threadpool(){
delete[]m_threads;
m_stop = true;
}
向工作队列中添加
添加时,注意线程同步
template<typename T>
bool my_threadpool<T>::append( T* request){
//添加任务 注意共享同步
my_queuelock.lock(); //pthread_mutex_lock(&m_nutex);
if(m_workqueue.size()>m_max_requests){ //超出最大量了
my_queuelock.unlock();
return false;
}
m_workqueue.push_back(request);
my_queuelock.unlock();
m_queuestat.post();
return true;
}
核心工作函数
整个线程的核心工作就是在此 循环的检查工作丢列中是否有就绪任务,从而进行对任务进行执行函数
template< typename T >
void my_threadpool< T >::run(){
while (!m_stop)
{
m_queuestat.wait(); //判断有无任务可做
my_queuelock.lock();
if(m_workqueue.empty()){
my_queuelock.unlock();
continue;
}
//有任务 去front 获取
T* request= m_workqueue.front();
m_workqueue.pop_front(); // 删除在队列的任务
my_queuelock.unlock();
if(!request){
continue;
}
request->process(); //任务执行类
}
}
总结
本章节 基于互斥量,条件变量,信号量 三种实现线程同步的方式,进行了封装。在此基础上,对线程池进行了封装,对整个线程池的工作模式进行了讲解,线程池我认为首先要理解的是整个工作模式,就像你去理解一个人一样,不可能单独的先去了解单独的部位,从整体的思想去理解后,再逐个细节上的吃透,记忆和使用的更加深
技术参考
本文部分技术点出处,C/C++Linux服务器开发/后台架构师:推荐免费订阅