Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器。
-
使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
-
使用状态机解析HTTP请求报文,支持解析GET和POST请求
-
访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
-
实现同步/异步日志系统,记录服务器运行状态
-
经Webbench压力测试可以实现上万的并发连接数据交换
Web服务器一般指服务器网站,指驻留于因特网上的某种类型计算机的程序,用于
处理浏览器等Web客户端的请求并返回响应响应。 目前主流有Apache、Nginx、IIS
本项目Web请求主要指HTTP协议
线程池的基本概念
线程池是一种
并发编程
技术,它能有效地管理并发的线程、减少资源占用和提高程序的性能。C++线程池通过
<thread>
库,结合C++ 11、14、17、20等的新特性,简化了
多线程编程
的实现。
线程池主要解决的问题
-
线程创建与销毁的开销以及线程竞争造成的性能瓶颈。通过预先创建一组线程并复用它们,线程池有效地降低了现成穿件和销毁的时间和资源消耗
-
通过管理线程并发数量,线程池有助于减少线程之间的竞争,增加资源利用率,提高程序运行的性能
-
总的来说,线程池是为了 提高性能与资源利用率
如何解决线程竞争问题?
过多的线程可能导致线程竞争,影响系统性能。线程池通过维护一个可控制的并发数量,有助于减少线程之间的竞争。
例如,当CPU密集型任务和IO密集型任务共存时,可以通过调整线程池资源,实现更高效的负载平衡
线程池工作原理
线程池在初始化时会预先创建一定数量的线程,这些线程将会被后续任务复用。线程数量根据实际需求和系统资源进行配置。创建线程池示例:
for (size_t i = 0; i < threadCount; ++i) {
threads.emplace_back(threadFunc, this);
}
任务队列与调度
线程池通过维护一个任务队列来管理执行任务。当线程池收到一个新任务时,将其加入任务队列。按照预定的策略从队列中执行任务(操作系统中任务队列类似),简单示例:
void ThreadPool::addTask(const Task& task) {
{
lock_guard<mutex> lock(queueMutex);
taskQueue.emplace(task);
}
condition.notify_one();
}
线程执行及回收
线程执行任务时,按照线程池的调度策略从任务队列中获取任务。任务完成后,线程放回线程池中等待下一个任务,而不是销毁。这种复用机制提高了资源利用率并降低了线程创建和销毁开销。简单例子
void ThreadPool::threadFunc() {
while (true) {
Task task;
{
unique_lock<mutex> lock(queueMutex);//使用`unique_lock`和`mutex`来锁定`queueMutex`,确保在多线程环境下对任务队列的访问是安全的。
condition.wait(lock, [this]() { return !taskQueue.empty() || terminate; });
if (terminate && taskQueue.empty()) {
break;
}
task = taskQueue.front();
taskQueue.pop();
}
task(); // Execute the task.
}
}
工作队列
struct NWORKER{
pthread_t threadid; //线程id
bool terminate; //是否需要结束该worker的标志
int isWorking; //该worker是否在工作
ThreadPool *pool; //隶属于的线程池
}
任务队列
任务实际上就是函数
struct NJOB{
void (*func)(void *arg); //任务函数
void *user_data; //函数参数
};
线程池创建
很明显线程池中需要两把锁,控制对任务队列操作的互斥锁,当任务队列有新任务时唤醒worker的条件锁
class ThreadPool{
private:
struct NWORKER{
pthread_t threadid;
bool terminate;
int isWorking;
ThreadPool *pool;
} *m_workers;
struct NJOB{
void (*func)(void *arg); //任务函数
void *user_data;
};
public:
//线程池初始化
//numWorkers:线程数量
ThreadPool(int numWorkers, int max_jobs);
//销毁线程池
~ThreadPool();
//面向用户的添加任务
int pushJob(void (*func)(void *data), void *arg, int len);
private:
//向线程池中添加任务
bool _addJob(NJOB* job);
//回调函数
static void* _run(void *arg);
void _threadLoop(void *arg);
private:
std::list<NJOB*> m_jobs_list;
int m_max_jobs; //任务队列中的最大任务数
int m_sum_thread; //worker总数
int m_free_thread; //空闲worker数
pthread_cond_t m_jobs_cond; //线程条件等待
pthread_mutex_t m_jobs_mutex; //为任务加锁防止一个任务被两个线程执行等其他情况
};
回调函数:在异步编程或事件驱动编程中,回调函数是常见的模式。当某个事件发生时(例如,用户点击按钮或定时器到期),系统或库可能会调用一个预先提供的函数指针(即回调函数)来处理该事件。因为回调函数可以接受任何类型的
void 指针作为参数,所以它可以用来传递事件相关的任何数据。
回调函数作为
pthread_create第三个参数
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
void *(*start_rtn)(void*),void *arg);必须传入一个静态函数,因为静态函数不会默认传递*this指针
主要函数实现:
void ThreadPool::_threadLoop(void *arg) {
NWORKER *worker = (NWORKER*)arg;
while (1){
//线程只有两个状态:执行\等待
//查看任务队列前先获取锁
pthread_mutex_lock(&m_jobs_mutex);
//当前没有任务
while (m_jobs_list.size() == 0) {
//检查worker是否需要结束生命
if (worker->terminate) break;
//条件等待直到被唤醒
pthread_cond_wait(&m_jobs_cond,&m_jobs_mutex);
}
//检查worker是否需要结束生命
if (worker->terminate){
pthread_mutex_unlock(&m_jobs_mutex);
break;
}
//获取到job后将该job从任务队列移出,免得其他worker过来重复做这个任务
struct NJOB *job = m_jobs_list.front();
m_jobs_list.pop_front();
//对任务队列的操作结束,释放锁
pthread_mutex_unlock(&m_jobs_mutex);
m_free_thread--;
worker->isWorking = true;
//执行job中的func
job->func(job->user_data);
worker->isWorking = false;
free(job->user_data);
free(job);
}
free(worker);
pthread_exit(NULL);
}
在while循环中不断执行:
-
如果有,则取出这个job,并将该job从任务队列中删除,且执行job中的func函数。
-
如果没有,调用 pthread_cond_wait函数等待job到来时被唤醒。
-
若当前 worker的 terminate为真,则退出循环结束线程。
添加任务函数:
bool ThreadPool::_addJob(NJOB *job) {
//尝试获取锁
pthread_mutex_lock(&m_jobs_mutex);
//判断队列是否超过任务数量上限
if (m_jobs_list.size() >= m_max_jobs){
pthread_mutex_unlock(&m_jobs_mutex);
return false;
}
//向任务队列添加job
m_jobs_list.push_back(job);
//唤醒休眠的线程
pthread_cond_signal(&m_jobs_cond);
//释放锁
pthread_mutex_unlock(&m_jobs_mutex);
return true;
}
如果不希望用户使用线程池的时候都需要自己定义job并添加到任务队列,job这种私密的关于内部实现的代码页不希望被看到,所以封装一层面向用户的添加任务函数。
//面向用户的添加任务
int ThreadPool::pushJob(void (*func)(void *), void *arg, int len) {
struct NJOB *job = (struct NJOB*)malloc(sizeof(struct NJOB));
if (job == NULL){
perror("malloc");
return -2;
}
memset(job, 0, sizeof(struct NJOB));
job->user_data = malloc(len);
memcpy(job->user_data, arg, len);
job->func = func;
_addJob(job);
return 1;
}
pthread_detach
函数是POSIX线程库中的一个重要函数,它用于将指定的线程标记为可分离状态。当一个线程被标记为可分离时,该线程的资源(如堆栈和线程描述符)在退出时可以自动被系统回收,而不需要等待其他线程使用
pthread_join
函数来释放它们。
我们可以在创建线程后调用pthread_detach函数,便于资源回收
ThreadPool::ThreadPool(int numWorkers, int max_jobs = 10) : m_sum_thread(numWorkers), m_free_thread(numWorkers), m_max_jobs(max_jobs){ //numWorkers:线程数量
if (numWorkers < 1 || max_jobs < 1){
perror("workers num error");
}
//初始化jobs_cond
if (pthread_cond_init(&m_jobs_cond, NULL) != 0)
perror("init m_jobs_cond fail\n");
//初始化jobs_mutex
if (pthread_mutex_init(&m_jobs_mutex, NULL) != 0)
perror("init m_jobs_mutex fail\n");
//初始化workers
m_workers = new NWORKER[numWorkers];
if (!m_workers){
perror("create workers failed!\n");
}
//初始化每个worker
for (int i = 0; i < numWorkers; ++i){
m_workers[i].pool = this;
int ret = pthread_create(&(m_workers[i].threadid), NULL, _run, &m_workers[i]);
if (ret){
delete[] m_workers;
perror("create worker fail\n");
}
if (pthread_detach(m_workers[i].threadid)){
delete[] m_workers;
perror("detach worder fail\n");
}
m_workers[i].terminate = 0;
}
}
析构函数:由于detach了所有线程,所以析构函数必须手动唤醒所有在条件等待的线程,并将worker的terminate设置为true
ThreadPool::~ThreadPool(){
//terminate值置1
for (int i = 0; i < m_sum_thread; i++){
m_workers[i].terminate = 1;
}
//广播唤醒所有线程
pthread_mutex_lock(&m_jobs_mutex);
pthread_cond_broadcast(&m_jobs_cond);
pthread_mutex_unlock(&m_jobs_mutex);
delete[] m_workers;
}
线程池中线程的合理数量:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
参考文章: