C++线程池学习

292 阅读7分钟

Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器。

  • 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型

  • 使用状态机解析HTTP请求报文,支持解析GET和POST请求

  • 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件

  • 实现同步/异步日志系统,记录服务器运行状态

  • 经Webbench压力测试可以实现上万的并发连接数据交换

Web服务器一般指服务器网站,指驻留于因特网上的某种类型计算机的程序,用于处理浏览器等Web客户端的请求并返回响应响应。 目前主流有Apache、Nginx、IIS

本项目Web请求主要指HTTP协议

线程池的基本概念

线程池是一种并发编程技术,它能有效地管理并发的线程、减少资源占用和提高程序的性能。C++线程池通过库,结合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 lock(queueMutex);

taskQueue.emplace(task);

}

condition.notify_one();

}

线程执行及回收

线程执行任务时,按照线程池的调度策略从任务队列中获取任务。任务完成后,线程放回线程池中等待下一个任务,而不是销毁。这种复用机制提高了资源利用率并降低了线程创建和销毁开销。简单例子

void ThreadPool::threadFunc() {

while (true) {

Task task;

{

unique_lock lock(queueMutex);//使用unique_lockmutex来锁定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数目

参考文章:

C/C++手撕线程池(线程池的封装和实现)_c++面试手撕一个线程池-CSDN博客