在实现线程池之前,首先对线程池中所需要用到的互斥锁、条件变量和信号量进行了简单的封装。
互斥锁、条件变量和信号量封装
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()
{
//这段代码确实是在检查互斥量是否被成功初始化,并在初始化失败时抛出异常。成功初始化返回0
if(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_t
int 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,还要传递一个时间t
bool 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_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值,生产+1,消费-1
int 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);
- 对信号量解锁,调用一次对信号量的值+1
int 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); - 调用一次对信号量的值+1
bool post()
{
return sem_post(&m_sem) == 0;
}
private:
sem_t m_sem; //信号量
};
#endif //LOCKER_H
2.线程池
线程池是一种用于管理和重用线程的并发编程技术。在软件开发中,线程池被用来处理大量的并发任务,以提高系统性能和资源利用率。
主要的组成部分包括:
-
线程池管理器(Thread Pool Manager):负责创建、销毁和管理线程池中的线程。它通常提供了添加任务、删除任务、调整线程池大小等接口,用于管理线程池的状态。
-
工作队列(Work Queue):用于存储需要执行的任务。当有任务需要执行时,线程从工作队列中获取任务并执行。工作队列可以是有限大小的队列,用于控制系统资源的使用。
-
线程池(Thread Pool):包含一组预先创建的线程,这些线程可以重复使用来执行任务。通过维护一组可重用的线程,线程池可以减少线程的创建和销毁开销,提高系统的性能和响应速度。
-
任务(Task):需要在线程池中执行的工作单元。任务可以是任意类型的计算、I/O 操作或其他类型的工作。
线程池的工作流程通常如下:
- 初始时,线程池会创建一定数量的线程,并将它们置于等待状态。
- 当有任务需要执行时,任务被添加到工作队列中。
- 线程池中的线程会不断地从工作队列中获取任务,并执行这些任务。
- 执行完任务后,线程会再次回到等待状态,等待下一个任务的到来。
- 当线程池不再需要时,可以销毁线程池中的线程,释放资源。
线程池的优势在于:
- 降低线程创建和销毁的开销。通过重用线程,减少了频繁创建和销毁线程的性能开销。
- 控制并发线程数量。线程池可以限制同时执行的线程数量,防止系统资源被过度占用。
- 提高系统响应速度。通过并发执行多个任务,可以提高系统的并发处理能力和响应速度。
在C++中,
this
是一个特殊的指针,它指向调用成员函数的对象。当你在一个类的非静态成员函数中使用this
时,它实际上指向调用该函数的实例。
this
指针允许你访问对象的所有成员,包括私有(private)和保护(protected)成员。以下是
this
指针的一些关键点:
隐含传递:当你调用一个类的非静态成员函数时,
this
指针会自动作为第一个参数传递给该函数。虽然你不需要显式地传递它,但在函数内部,你可以使用this
来引用调用该函数的对象。类型:
this
指针的类型是指向类类型的指针。例如,如果你有一个名为MyClass
的类,那么this
的类型就是MyClass*
。使用场景:
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方法,类型为T
bool 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_threads
delete[] 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指针来访问。
* 调用方式:可以通过类名和作用域解析运算符::来调用静态成员函数,也可以通过类的对象来调用(尽管这样做并不常见)。
静态成员函数不能直接访问类的非静态成员变量,因为静态成员函数不与类的任何特定实例关联,而非静态成员变量是与类的实例关联的。
但是,有一些方法可以间接地访问非静态成员变量:
通过参数传递:你可以将非静态成员变量的引用或指针作为参数传递给静态成员函数。
这样,静态成员函数就可以通过这个参数来访问和修改非静态成员变量。
通过类的实例:如果静态成员函数能够获得类的某个实例的引用或指针,那么它可以通过这个实例来访问非静态成员变量。
这通常是通过将实例作为参数传递给静态成员函数来实现的。
* */