TinyWebServer项目小白逐行解析第四集——threadpool

对于像我这样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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值