C++线程池学习

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到来时被唤醒。
  • 若当前 workerterminate为真,则退出循环结束线程。
添加任务函数:
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数目
参考文章:
  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值