Webserver(1): C++实现线程池

2024-03-03 18:12
文章标签 c++ 实现 线程 webserver

本文主要是介绍Webserver(1): C++实现线程池,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在实现线程池之前,首先对线程池中所需要用到的互斥锁、条件变量和信号量进行了简单的封装。

互斥锁、条件变量和信号量封装

locker.h头文件如下(已详细注释)

/*
这里面对互斥锁,条件变量和信号量进行了封装
保证工作队列的线程同步与数据安全
*/#ifndef LOCKER_H
#define LOCKER_H
/*
这是一个简单的C或C++头文件保护(header guard)机制,用于防止头文件被多次包含(include)。
#ifndef LOCKER_H:#ifndef是预处理指令,用于检查LOCKER_H这个宏是否已经定义。如果LOCKER_H没有被定义,那么后面的代码(直到#endif)会被编译器包含(include)。
#define LOCKER_H:这行代码定义了一个宏LOCKER_H。一旦这个宏被定义,再次遇到#ifndef LOCKER_H时,由于LOCKER_H已经被定义,所以其后的代码不会被再次包含。
#endif:这是结束#ifndef预处理的指令。
这种机制确保了在同一个编译单元中,头文件只被包含一次,避免了由于多次包含同一个头文件而可能导致的各种问题,如重复定义、多重继承等。
*/#include <pthread.h>
#include <exception>   //异常处理
#include <semaphore.h> //信号量
#include <stdexcept>   //std::runtime_error 是定义在 <stdexcept> 头文件中的一个异常类//可以使用互斥量(mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。
//任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败
//一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。
/*初始化互斥量后,你可以使用 pthread_mutex_lock 函数来锁定互斥量,使用 pthread_mutex_unlock 函数来解锁互斥量。锁定互斥量的线程将独占对共享资源的访问,直到它解锁该互斥量。其他尝试锁定该互斥量的线程将被阻塞,直到互斥量被解锁。* */
//1.互斥锁类,应该确保一个线程在访问资源的时候,另外的线程不能同时访问这些资源
class Locker{
public://1.1 构造函数,对互斥量进行初始话Locker(){//这段代码确实是在检查互斥量是否被成功初始化,并在初始化失败时抛出异常。成功初始化返回0if(pthread_mutex_init(&m_mutux, NULL) != 0){throw std::runtime_error("Failed to initialize mutex");}}//1.2 析构函数,对互斥量进行消耗~Locker(){pthread_mutex_destroy(&m_mutux);}//1.3 上锁函数bool lock(){return pthread_mutex_lock(&m_mutux) == 0;  //上锁成功返回0}//1.4 解锁函数bool unlock(){return pthread_mutex_unlock(&m_mutux) == 0;}//1.5 get函数获取互斥量pthread_mutex_t * get(){return &m_mutux;}/** 在C++中,pthread_mutex_t 是一个结构体类型,通常用于POSIX线程编程中的互斥量。* 当你通过函数返回一个 pthread_mutex_t 类型的值时,你实际上是在返回这个结构体的一个副本。* 然而,对于互斥量这样的类型,返回其副本通常是没有意义的,因为互斥量的状态(如锁定或未锁定)不能通过简单地复制结构体来传递。因此,当你想从一个函数返回一个互斥量以便在其他地方使用时,通常会返回指向互斥量的指针。这样,调用者可以通过这个指针来操作原始的互斥量对象,而不是它的一个副本。* */
private:pthread_mutex_t m_mutux;  //互斥量
};//2. 条件变量类
/*条件变量(Condition Variables)是线程同步的一种机制,它允许一个或多个线程等待某个条件成立,或者在某个条件成立后唤醒一个或多个等待的线程。条件变量通常与互斥锁(Mutex)一起使用,以避免竞争条件和保证线程安全。条件变量的类型 pthread_cond_tint pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);等待,调用了该函数,线程会阻塞。当这个函数调用阻塞等待的时候,会对互斥锁进行解锁,否则生产者拿不到互斥锁。解除阻塞时,重新加锁int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);- 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。int pthread_cond_signal(pthread_cond_t *cond);- 唤醒一个或者多个等待的线程int pthread_cond_broadcast(pthread_cond_t *cond);- 唤醒所有的等待的线程*//*条件变量(Condition Variable)是操作系统提供的一种线程间同步机制,用于在多线程环境中实现线程的等待和唤醒操作。它通常与互斥锁(Mutex)结合使用,用于实现复杂的线程同步。条件变量的原理如下:线程在进入临界区前先获取互斥锁。当某个条件不满足时,线程调用条件变量的等待(wait)函数,
并释放之前获取到的互斥锁,然后进入阻塞状态等待被唤醒。当其他线程满足了该条件时,调用条件变量的通知或广播(broadcast)函数来唤醒一个或多个等待中的线程。被唤醒的线程重新获得互斥锁,并检查条件是否满足。如果满足,则继续执行;如果不满足,则再次进入等待状态。条件变量的作用是用于多线程之间关于共享数据状态变化的通信。当一个动作需要另外一个动作完成时才能进行,
即:当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量。操作系统是主动调用者,而条件变量其实是操作系统预留出的接口。
因而这里主要是去考虑记录谁在等待、记录谁要唤醒、如何唤醒的问题。条件变量是一种等待机制,每一个条件变量对应一个等待原因与等待队列。* */
class Cond{
public://2.1 构造函数,初始化Cond(){if(pthread_cond_init(&m_cond, NULL) != 0){throw std::runtime_error("Failed to initialize Condition Variables");}}//2.2 析构函数~Cond(){pthread_cond_destroy(&m_cond);}//2.3 条件变量要配合互斥锁使用,因此需要传递一个互斥锁指针类型bool wait(pthread_mutex_t * mutex){return pthread_cond_wait(&m_cond, mutex) == 0;}//2.4 timewait,还要传递一个时间tbool timewait(pthread_mutex_t * mutex, struct timespec t){return pthread_cond_timedwait(&m_cond, mutex, &t) == 0;}//2.5 唤醒一个或者多个等待的线程bool signal(){return pthread_cond_signal(&m_cond) == 0;}//2.6 唤醒所有的等待的线程bool broadcast(){return pthread_cond_broadcast(&m_cond) == 0;}private:pthread_cond_t m_cond;
};//3. 信号量类
/*信号量(Semaphore)是一种用于控制多个线程或进程对共享资源访问的同步机制。
它可以看作是一个计数器,用于表示可用资源的数量。信号量的主要操作包括P操作(等待)和V操作(释放)。P操作(Wait):当一个线程或进程需要访问共享资源时,它首先会执行P操作。这个操作会将信号量的值减1,
表示一个资源被占用。如果信号量的值大于0,表示还有可用资源,
线程或进程可以继续执行;如果信号量的值为0,表示没有可用资源,线程或进程将被阻塞,直到有资源可用。V操作(Signal):当一个线程或进程完成对共享资源的访问后,它会执行V操作。这个操作会将信号量的值加1,
表示一个资源被释放。如果有其他线程或进程正在等待该资源(即被P操作阻塞),那么它们将被唤醒并继续执行。信号量的类型 sem_tint sem_init(sem_t *sem, int pshared, unsigned int value);- 初始化信号量- 参数:- sem : 信号量变量的地址- pshared : 0 用在线程间 ,非0 用在进程间- value : 信号量中的值,生产+1,消费-1int sem_destroy(sem_t *sem);- 释放资源int sem_wait(sem_t *sem);- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)int sem_trywait(sem_t *sem);int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);int sem_post(sem_t *sem);- 对信号量解锁,调用一次对信号量的值+1int sem_getvalue(sem_t *sem, int *sval);信号量的工作原理基于两种基本操作:P(等待)操作和V(发送信号)操作。P操作用于获取信号量,即减少信号量值。如果信号量值大于0,表示资源可用,进程或线程可以访问该资源,并将信号量值减1;如果信号量值等于0,表示资源已被占用,进程或线程需要等待其他进程或线程释放资源,并将自己挂起,直到信号量值变为正数。V操作用于释放信号量,即增加信号量值。如果有进程或线程正在等待该信号量,则唤醒其中一个进程或线程,使其继续执行。
*/
class Sem{
public:Sem(){if(sem_init(&m_sem, 0, 0) != 0){throw std::runtime_error("Failed to initialize Semaphore");}}~Sem(){sem_destroy(&m_sem);}// int sem_wait(sem_t *sem); - 调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)bool wait(){return sem_wait(&m_sem) == 0;}// int sem_post(sem_t *sem); - 调用一次对信号量的值+1bool post(){return sem_post(&m_sem) == 0;}private:sem_t m_sem;  //信号量
};#endif //LOCKER_H

2.线程池

线程池是一种用于管理和重用线程并发编程技术。在软件开发中,线程池被用来处理大量的并发任务,以提高系统性能和资源利用率。

主要的组成部分包括:

  1. 线程池管理器(Thread Pool Manager):负责创建、销毁和管理线程池中的线程。它通常提供了添加任务、删除任务、调整线程池大小等接口,用于管理线程池的状态。

  2. 工作队列(Work Queue)用于存储需要执行的任务。当有任务需要执行时,线程从工作队列中获取任务并执行。工作队列可以是有限大小的队列,用于控制系统资源的使用。

  3. 线程池(Thread Pool):包含一组预先创建的线程,这些线程可以重复使用来执行任务。通过维护一组可重用的线程,线程池可以减少线程的创建和销毁开销,提高系统的性能和响应速度。

  4. 任务(Task):需要在线程池中执行的工作单元。任务可以是任意类型的计算、I/O 操作或其他类型的工作。

线程池的工作流程通常如下:

  • 初始时,线程池会创建一定数量的线程,并将它们置于等待状态
  • 当有任务需要执行时,任务被添加到工作队列中。
  • 线程池中的线程会不断地从工作队列中获取任务,并执行这些任务。
  • 执行完任务后,线程会再次回到等待状态,等待下一个任务的到来
  • 线程池不再需要时,可以销毁线程池中的线程,释放资源

线程池的优势在于:

  • 降低线程创建和销毁的开销。通过重用线程,减少了频繁创建和销毁线程的性能开销。
  • 控制并发线程数量。线程池可以限制同时执行的线程数量,防止系统资源被过度占用。
  • 提高系统响应速度。通过并发执行多个任务,可以提高系统的并发处理能力和响应速度。

在C++中,this 是一个特殊的指针,它指向调用成员函数的对象。当你在一个类的非静态成员函数中使用 this 时,它实际上指向调用该函数的实例。

this 指针允许你访问对象的所有成员,包括私有(private)和保护(protected)成员。

以下是 this 指针的一些关键点:

  1. 隐含传递:当你调用一个类的非静态成员函数时,this 指针会自动作为第一个参数传递给该函数。虽然你不需要显式地传递它,但在函数内部,你可以使用 this 来引用调用该函数的对象。

  2. 类型this 指针的类型是指向类类型的指针。例如,如果你有一个名为 MyClass 的类,那么 this 的类型就是 MyClass*

  3. 使用场景this 指针通常用于以下情况:

    • 当成员函数的参数名和类的成员变量名相同时,为了避免歧义,可以使用 this 指针来明确指代类的成员变量。
    • 当你想在成员函数中返回对象本身(通常用于链式操作)时,可以使用 return *this;
    • 在某些情况下,你可能想将 this 指针传递给其他函数或方法。
      class MyClass {  
      public:  int value;  MyClass(int val) : value(val) {}  // 使用 this 指针来访问和修改成员变量  void setValue(int newVal) {  this->value = newVal; // this-> 是可选的,但在某些情况下可以帮助提高代码的可读性  }  // 返回对象本身,以便进行链式操作  MyClass* incrementValue() {  this->value++;  return this;  }  
      };  int main() {  MyClass obj(10);  obj.setValue(20);  obj.incrementValue()->incrementValue(); // 链式操作  return 0;  
      }

线程池代码threadpool.h

//线程池的实现
#ifndef THREADPOOL_H
#define THREADPOOL_H#include <pthread.h>
#include <list>
#include "locker.h"
#include <exception>
#include <cstdio>
#include <stdexcept>
#include <iostream>
using namespace std;//定义成模板类,是为了代码的复用,
//任务可能是不同的,T就是任务类
template<typename T>
class Threadpool{
public://1 构造函数,初始化线程数量, 请求队列中最多允许的,等待处理的请求数量Threadpool(int thread_number = 8, int max_requests = 10000);//2 析构函数~Threadpool();//3 向工作队列中去添加任务,append方法,类型为Tbool append(T * request);private://静态函数,不能访问非静态的成员变量,线程所要执行实现的功能static void* worker(void* arg);/*** */void run();private://1 线程的数量int m_thread_number;//2 线程池数组,存储创建线程的pid,大小与线程数量一致pthread_t * m_threads;//3 工作队列中最多允许的,等待处理的请求数量int m_max_requests;//4 工作队列std::list<T*> m_workqueue;/** 内存管理:使用指针允许你更灵活地管理内存。例如,如果你有一个大型对象或动态分配的对象,* 将其存储在std::list<T>中可能会导致不必要的内存复制,因为std::list在插入和删除元素时可能需要重新分配内存。* 使用指针可以避免这种复制,因为实际上你只是在复制指针(一个小的内存地址),而不是整个对象。* *///5 互斥锁Locker m_queue_mutex;//6 信号量用来判断是否有任务需要处理Sem m_queue_sem;//7 是否结束线程bool m_stop;
};//1 构造函数的类外初始化,在这个里面要创建出来线程
template<typename T>
Threadpool<T>::Threadpool(int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests),
m_stop(false), m_threads(nullptr)
{//1. 参数是否正确的判断if(thread_number <= 0 || max_requests <= 0){throw std::runtime_error("Failed to initialize Threadpool");}//2. 根据线程的数量创建出线程池数组,存储创建线程的pid,析构的时候需要销毁m_threads = new pthread_t[thread_number];if(!m_threads){throw std::runtime_error("m_threads Error");}//3. 创建thread_number,线程pid存储在m_threads中,并设置为线程分离for(int i = 0; i < thread_number; i++){cout<<"create" << i << " th thread"<<endl;//线程执行的代码在worker中,是个静态函数,创建的时候,并没有显示指定存储子线程的tid的变量,而是直接放在数组中if(pthread_create(m_threads + i, NULL, worker, this) != 0){//子线程创建失败,删掉这个数组m_threadsdelete[] m_threads;/** 在C++中,delete 和 delete[] 是用于释放动态分配的内存的运算符,但它们的使用场景有所不同。* delete:用于释放通过 new 运算符单个分配的对象。delete[]:用于释放通过 new[] 运算符分配的对象数组。* */throw std::runtime_error("pthread_create Error");}//设置线程分离if(pthread_detach(m_threads[i]) != 0){delete[] m_threads;throw std::runtime_error("pthread_detach Error");}}
}//2 析构函数
template<typename T>
Threadpool<T>::~Threadpool()
{delete[] m_threads;m_stop = true;
}//3 向工作队列中去添加任务,append方法,类型为T,并且需要确保线程同步
template<typename T>
bool Threadpool<T>::append(T *request)
{//1 上锁m_queue_mutex.lock();//2 如果工作队列中的大小大于最大的工作队列中最多允许的,等待处理的请求数量,解锁,返回错误,处理不了了if(m_workqueue.size() > m_max_requests){m_queue_mutex.unlock();return false;}//3 向工作队列中添加m_workqueue.push_back(request);m_queue_mutex.unlock(); //解锁m_queue_sem.post(); //信号量增加,说明工作队列中有了新任务
}//4 线程执行的代码在worker中,是个静态函数
/*
静态函数,它不能访问非静态的成员函数
if (pthread_create(m_threads + i, NULL, worker, NULL) != 0);
在创建线程的时候
if (pthread_create(m_threads + i, NULL, worker,this) != 0); this代表本类对象,是Threadpool类型对象* */
template<typename T>
void* Threadpool<T>::worker(void * arg)
{Threadpool * pool = (Threadpool *) arg;pool->run();return pool;
}//运行函数run,在工作队列中去任务,做任务
template<typename T>
void Threadpool<T>::run()
{while(!m_stop){//工作队列中的信号量-1,如果为0则阻塞在这m_queue_sem.wait();//加锁m_queue_mutex.lock();//如果工作队列为空就解锁if(m_workqueue.empty()){m_queue_mutex.unlock();continue;}//取第一个任务T* request = m_workqueue.front();m_workqueue.pop_front();m_queue_mutex.unlock();if(!request){continue;}request->process(); //调用process函数执行任务}}#endif //THREADPOOL_H/** C++中的静态函数* 在C++中,静态成员函数(Static Member Functions)是类的一部分,但它们与类的实例(对象)无关。* 与类关联,而非对象关联:静态成员函数属于类本身,而不是类的某个特定对象。因此,它们可以在没有创建类对象的情况下被调用。* 访问限制:静态成员函数只能直接访问静态成员变量和其他静态成员函数,不能访问类的非静态成员变量和非静态成员函数,除非通过类的实例或指针/引用。* 不隐藏this指针:静态成员函数不接收this指针,因此它们不能访问类的非静态成员,因为这些成员需要通过this指针来访问。* 调用方式:可以通过类名和作用域解析运算符::来调用静态成员函数,也可以通过类的对象来调用(尽管这样做并不常见)。
静态成员函数不能直接访问类的非静态成员变量,因为静态成员函数不与类的任何特定实例关联,而非静态成员变量是与类的实例关联的。但是,有一些方法可以间接地访问非静态成员变量:通过参数传递:你可以将非静态成员变量的引用或指针作为参数传递给静态成员函数。
这样,静态成员函数就可以通过这个参数来访问和修改非静态成员变量。通过类的实例:如果静态成员函数能够获得类的某个实例的引用或指针,那么它可以通过这个实例来访问非静态成员变量。
这通常是通过将实例作为参数传递给静态成员函数来实现的。* */

这篇关于Webserver(1): C++实现线程池的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/770477

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景