对于像我这样0项目经验的小白想学习Linux Web服务器来说,WebServer项目无疑是最好的一个Web服务器项目。我站在各位大神的肩膀上记录一步步剖析这个项目的过程,也给其他跟我一样苦于看源码头疼的初学者一点帮助。
Github链接:qinguoyi/TinyWebServer: :fire: Linux下C++轻量级WebServer服务器
前置知识
1.什么是线程?什么是线程池?
线程
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等,但每个线程都有自己独立的执行栈和程序计数器,能够独立执行代码。线程可以并发执行,从而提高程序的执行效率,尤其在处理多任务或需要异步操作的场景中非常有用。
线程池
线程池(Thread Pool)是一种线程使用模式。线程池会预先创建一定数量的线程,当有任务提交时,从线程池中获取一个空闲线程来执行该任务。任务执行完毕后,线程不会被销毁,而是返回线程池等待下一个任务。线程池通过管理线程的生命周期,避免了频繁创建和销毁线程带来的开销,提高了系统的性能和资源利用率。
2.为什么要使用线程池,它和sql_connectionpool的区别在哪?
使用线程池的原因
1.减少线程创建和销毁的开销:线程的创建和销毁是比较昂贵的操作,涉及到系统资源的分配和回收。线程池预先创建好一定数量的线程,避免了频繁创建和销毁线程的开销,提高了系统的响应速度。
2.提高系统的稳定性:线程池可以限制线程的数量,避免因创建过多线程导致系统资源耗尽,从而提高系统的稳定性。
3.便于线程管理:线程池可以统一管理线程的生命周期、调度和监控,方便开发人员进行线程的管理和维护。
线程池和sql_connectionpool的区别
1.功能不同:线程池主要用于管理和调度线程,提高线程的使用效率;而sql_connectionpool(数据库连接池)主要用于管理和复用数据库连接,减少数据库连接的创建和销毁开销。
2.管理对象不同:**线程池管理的是线程,每个线程可以执行不同的任务;而sql_connectionpool管理的是数据库连接,每个连接用于与数据库进行交互。
3.应用场景不同:**线程池适用于需要处理大量并发任务的场景,如 Web 服务器处理客户端请求;而sql_connectionpool适用于需要频繁访问数据库的场景,如 Web 应用程序中对数据库的读写操作。
3.线程池有什么优势?有什么缺点?
优势
1.提高性能:**减少线程创建和销毁的开销,提高了系统的响应速度和处理能力。
2.资源管理:**可以限制线程的数量,避免系统资源耗尽,提高系统的稳定性。
3.便于维护:**统一管理线程的生命周期、调度和监控,方便开发人员进行线程的管理和维护。
4.提高并发度:**通过并发执行任务,充分利用多核处理器的性能,提高系统的并发处理能力。
缺点
1.复杂度增加:**线程池的实现和管理相对复杂,需要考虑线程的同步、调度、异常处理等问题,增加了开发和维护的难度。
2.资源浪费:**如果线程池的大小设置不合理,可能会导致线程资源的浪费。例如,线程池中的线程数量过多,会增加系统的上下文切换开销;线程数量过少,可能无法充分利用系统资源。
3.任务调度问题:**线程池中的任务调度需要考虑任务的优先级、执行顺序等问题,如果调度不合理,可能会导致某些任务长时间得不到执行。
4.它的适用场景应该是什么?
1.高并发任务处理:当系统需要处理大量并发任务时,如 Web 服务器处理客户端请求、消息队列处理消息等,使用线程池可以提高系统的处理能力和响应速度。
2.异步任务执行:对于一些耗时的任务,如文件读写、网络请求、数据库操作等,可以将这些任务提交到线程池中异步执行,避免阻塞主线程,提高系统的并发性能。
3.资源有限的系统:在资源有限的系统中,使用线程池可以限制线程的数量,避免因创建过多线程导致系统资源耗尽,提高系统的稳定性。
5.与线程相关的函数:
1.int pthread_create(pthread_t *thread, const pthread_attr_t* attr, void*(*start_routinue)(void*), void* arg);
//创建一个线程,thread为线程标示符,,attr为设置新线程的属性,start_routine为指定线程运行的函数
//arg为指定线程运行函数的参数
//返回值,成功0,失败错误码。
2.void pthread_exit(void* retval);
//线程一旦创建好,内核就会调度内核线程执行start_coutine函数指针所指向的函数,执行完毕之后最好执行退出函数。
//该函数能退出线程,并且执行完毕后不会返回,且不会执行失败。
3.int pthread_join(pthread_t thread, void** retval);
//回收其他线程,,thread目标线程的标识符,retval为目标线程返回的退出信息
//返回值,成功0,失败错误码。
4.int pthread_cancel(pthread_t thread);
//该函数的作用是异常终止线程,即取消线程。其中thread参数是目标线程标识符,成功返回0,失败返回错误码。
//接收到取消请求的目标线程可以决定本线程是否允许被取消,以及如何取消。
Linux中各线程属性就定义在该字符数组中,线程库定义了一系列函数来操作pthread_attr_t类型的变量来获取和设置线程属性。
#include<bits/pthreadtypes.h>
#define SIZEOF_PTHREAD_ATTR_T 36
typedef union{
char size[SIZEOF_PTHREAD_ATTR_T];
long int align;
}pthread_attr_t;
6.线程的实现及其相关的简单的示例
线程实际上以pthread_t实现,头文件包含。
#include <pthread.h>
1. 定义和作用
pthread_t本质上是一个数据类型,用于唯一标识一个线程。每个线程在创建时,都会被分配一个pthread_t类型的标识符,通过这个标识符可以对线程进行管理、操作和控制,例如等待线程结束(pthread_join)、分离线程(pthread_detach)等。
在不同的系统和实现中,pthread_t的具体实现可能不同,它可能是一个整数、结构体或者其他数据类型,我们通常不需要关心其内部细节,只需要将其作为一个用于标识线程的句柄来使用即可。
2. 创建线程并使用pthread_t
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// 线程执行的函数
void* thread_function(void* arg) {
int thread_id = *((int*)arg);
printf("Thread %d is running.\n", thread_id);
pthread_exit(NULL);
}
int main() {
pthread_t thread;
int thread_arg = 1;
// 创建线程
int ret = pthread_create(&thread, NULL, thread_function, &thread_arg);
if (ret != 0) {
perror("pthread_create");
return EXIT_FAILURE;
}
// 等待线程结束
ret = pthread_join(thread, NULL);
if (ret != 0) {
perror("pthread_join");
return EXIT_FAILURE;
}
printf("Main thread continues.\n");
return EXIT_SUCCESS;
}
在上述代码中:
pthread_create函数用于创建一个新线程,第一个参数&thread是指向pthread_t类型变量的指针,用于接收新创建线程的标识符。
pthread_join函数用于等待指定的线程(通过pthread_t标识符thread指定)结束,并回收其资源。
3. 线程分离
有时候,我们可能希望线程在结束时自动释放资源,而不需要主线程显式地调用pthread_join来等待它,这时可以使用pthread_detach函数将线程分离。示例如下:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void* thread_function(void* arg) {
int thread_id = *((int*)arg);
printf("Thread %d is running.\n", thread_id);
pthread_exit(NULL);
}
int main() {
pthread_t thread;
int thread_arg = 1;
int ret = pthread_create(&thread, NULL, thread_function, &thread_arg);
if (ret != 0) {
perror("pthread_create");
return EXIT_FAILURE;
}
// 将线程分离
ret = pthread_detach(thread);
if (ret != 0) {
perror("pthread_detach");
return EXIT_FAILURE;
}
printf("Main thread continues without waiting for the thread.\n");
return EXIT_SUCCESS;
}
- pthread_join
- 功能:pthread_join函数的主要功能是阻塞调用线程,直到指定的线程结束。调用线程会暂停执行,等待目标线程执行完毕后,调用线程才会继续执行后续代码。
- 用途:常用于需要确保某个线程执行完毕后再进行后续操作的场景,比如在主线程中等待子线程完成一些数据处理任务后,再对处理结果进行汇总或进一步处理。
- pthread_detach
- 功能:pthread_detach函数的作用是将指定的线程标记为分离状态。处于分离状态的线程在结束时,系统会自动回收其占用的资源,不需要其他线程调用pthread_join来进行资源回收。
- 用途:适用于那些不需要等待其结束,也不需要获取其返回值的线程。例如,一些后台线程执行一些独立的、不需要与其他线程同步的任务,使用pthread_detach可以让这些线程在结束时自动释放资源,避免资源泄漏。
7.pthread_t 和pthread_cond_t的区别
简单来说,pthread_t是每个线程的身份证,而pthread_cond_t用于实现项目中的条件变量cond,用于控制单一资源的占用和释放的互斥锁。
1. 基本定义与用途
- pthread_t
- 定义:pthread_t是一种用于表示线程标识符的数据类型。在使用 Pthreads 库创建线程时,系统会为每个新线程分配一个唯一的pthread_t类型的标识符,通过这个标识符可以对线程进行操作和管理。
- 用途:主要用于线程的创建、等待、分离等操作。例如,在调用pthread_create函数创建线程时,需要传入一个指向pthread_t类型变量的指针,用于接收新线程的标识符;使用pthread_join函数等待线程结束时,需要传入该线程的pthread_t标识符。
- pthread_cond_t
- 定义:pthread_cond_t是一种用于表示条件变量的数据类型。条件变量是一种线程同步机制,用于线程间的协调和通信,允许线程在某个条件不满足时等待,当条件满足时被唤醒。
- 用途:通常与互斥锁(pthread_mutex_t)一起使用,用于实现线程的等待和唤醒机制。例如,当一个线程需要等待某个条件满足才能继续执行时,可以使用pthread_cond_wait函数进入等待状态;当另一个线程改变了条件并通知等待的线程时,可以使用pthread_cond_signal或pthread_cond_broadcast函数唤醒等待的线程。
threadpool.h
相关函数/API
threadpool(int actor_model, connection_pool *connPool,
int thread_number = 8, int max_request = 10000);
//初始化线程池,包括设置线程数量、最大请求数、数据库连接池和模型类型。
//thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量
~threadpool();
//析构函数
bool append(T *request, int state);
bool append_p(T *request);
//将任务添加到任务队列中
static void *worker(void *arg);
void run();
//工作线程运行的函数,它不断从工作队列中取出任务并执行
相关成员变量
int m_actor_model; //模型切换
int m_thread_number; //线程池中的线程数
int m_max_requests; //请求队列中允许的最大请求数
pthread_t *m_threads; //描述线程池的数组,其大小为m_thread_number
std::list<T *> m_workqueue; //请求队列
connection_pool *m_connPool; //数据库,用于线程处理任务时与数据库进行数据交换
locker m_queuelocker; //保护请求队列的互斥锁
sem m_queuestat; //是否有任务需要处理,共享信号量
代码详述
使用模板,适应多种线程需求。
1.threadpool<T>::threadpool //初始化
- actor_model 表示模型切换,connPool 是数据库连接池指针,thread_number 是线程池中的线程数量,max_requests 是请求队列中允许的最大请求数。初始化列表用于初始化类的成员变量,将传入的参数赋值给对应的成员变量,并将 m_threads 初始化为 NULL。
- 使用 for 循环遍历线程数组,为每个线程调用 pthread_create 函数创建线程。
- pthread_create 函数的参数分别为:线程数组的指针、线程属性(这里为 NULL)、线程执行的函数(worker)和传递给线程函数的参数(this,即当前线程池对象的指针)。
- 如果 pthread_create 函数返回值不为 0,表示线程创建失败,此时释放线程数组的内存并抛出一个标准异常。
- 调用 pthread_detach 函数将线程设置为分离状态,使线程结束后自动回收资源。
- 如果 pthread_detach 函数返回值不为 0,表示设置分离状态失败,此时释放线程数组的内存并抛出一个标准异常。
template <typename T>
threadpool<T>::threadpool( int actor_model, connection_pool *connPool,
int thread_number, int max_requests) :
m_actor_model(actor_model),m_thread_number(thread_number),
m_max_requests(max_requests), m_threads(NULL),
m_connPool(connPool)
{
if (thread_number <= 0 || max_requests <= 0)
throw std::exception();
m_threads = new pthread_t[m_thread_number];
if (!m_threads)
throw std::exception();
for (int i = 0; i < thread_number; ++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();
}
}
}
2.threadpool<T>::~threadpool() //析构函数
template <typename T>
threadpool<T>::~threadpool()
{
delete[] m_threads;
}
3.bool threadpool<T>::append(T request, int state) //添加任务
这里的state为任务模式,0为读,1为写,这是在后续的http请求中讲解。
template <typename T>
bool threadpool<T>::append(T *request, int state)
{
m_queuelocker.lock();
if (m_workqueue.size() >= m_max_requests)
{
m_queuelocker.unlock();
return false;
}
request->m_state = state;
m_workqueue.push_back(request);
m_queuelocker.unlock();
m_queuestat.post();
return true;
}
请求队列m_workqueue用list实现,以双向链表实现
list.size()//返回当前大小
list.front() //返回头节点
list.back() //返回尾节点
list.push_back() //尾部插入节点
list.push_front() //头部插入节点
list.pop_front()//删除头节点
list.pop_back()//删除尾节点
m_queuestat为sem信号量,使得信号量+1,用于通知其他线程。
1.sem_wait(&sem) ;
//等待信号量,当信号量时0时,程序阻塞等待。
//当有相关操作使得信号量加1,当信号量大于0时,程序即可继续运行,并使得信号量-1。
2.sem_post(&sem);
//发送信号量,作用为信号量的值加1,实现线程的同步控制。
3.sem_init(sem_t *__sem, int __pshared, unsigned int __value);
//sem为指向sem结构体的指针,pshared不为0时此信号量在进程间共享,
//否则为当前进程的所有线程共享,Linux系统不支持在进程间共享信号量。value给出了信号量的初始值。
4.sem_destroy(&sem);
//释放信号量sem
4.bool threadpool<T>::append_p(T request) //添加任务
原理同上,但是没有了任务模式变量
template <typename T>
bool threadpool<T>::append_p(T *request)
{
m_queuelocker.lock();
if (m_workqueue.size() >= m_max_requests)
{
m_queuelocker.unlock();
return false;
}
m_workqueue.push_back(request);
m_queuelocker.unlock();
m_queuestat.post();
return true;
}
5.void threadpool<T>::worker(void arg)
对于每一个创建的新的线程都将执行worker函数。
其中包含了run( )函数,用于处理读写消息。
template <typename T>
void *threadpool<T>::worker(void *arg)
{
threadpool *pool = (threadpool *)arg;
pool->run();
return pool;
}
6.void threadpool<T>::run() //负责从工作队列中取出任务并执行之
这里的操作与后续的http类相关。
template <typename T>
void threadpool<T>::run() //负责从工作队列中取出任务并执行之
{
while (true)
{
m_queuestat.wait(); //信号量减1,若信号量为0,则阻塞 ,wait 方法用于等待信号量。
//当调用 sem_wait 函数时,如果信号量的值大于 0,该函数会将信号量的值减 1 并立即返回;如果信号量的值为 0,调用线程会被阻塞,直到信号量的值大于 0。
m_queuelocker.lock(); //保护工作队列的互斥锁
if (m_workqueue.empty())
{
m_queuelocker.unlock();
continue;
}
T *request = m_workqueue.front();
m_workqueue.pop_front();
m_queuelocker.unlock(); //解锁
if (!request)
continue;
if (1 == m_actor_model)
{
if (0 == request->m_state) //读为0,写为1
{
if (request->read_once())
{
request->improv = 1;
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();
}
else
{
request->improv = 1;
request->timer_flag = 1;
}
}
else
{
if (request->write())
{
request->improv = 1;
}
else
{
request->improv = 1;
request->timer_flag = 1;
}
}
}
else
{
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();
}
}
}
connectionRAII 类:这是一个资源获取即初始化(RAII)风格的类,用于管理数据库连接。RAII 是 C++ 中一种常用的编程技术,通过将资源的生命周期绑定到对象的生命周期,确保资源在对象创建时获取,在对象销毁时释放,从而避免资源泄漏。
connectionRAII mysqlcon(&request->mysql, m_connPool);
创建了一个 connectionRAII 对象 mysqlcon,它会自动从数据库连接池 m_connPool 中获取一个数据库连接,并将其关联到 request->mysql 上。当 mysqlcon 对象超出作用域时,它会自动将数据库连接释放回连接池。
connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool){
*SQL = connPool->GetConnection();
conRAII = *SQL;
poolRAII = connPool;
}