C++线程封装与线程池实现

基础

之前在Linux阶段学习APUE时,我们学过进程的相关概念、后面又学了线程的相关概念。而现在我们主要是来进行
线程的进一步学习。之前在Linux里面使用的是C语言面向过程的思想,从现在开始我们需要使用C++面向对象的思想进行封装,但是在封装之前我们先来回顾一下线程相关的API。

线程的创建

#include <pthread.h>
//函数原型
int pthread_create(pthread_t *thread, 
		const pthread_attr_t *attr,
		void *(*start_routine) (void *), 
		void *arg);
//参数说明
thread:线程id
attr:线程属性,默认为空
start_routine:线程入口函数
arg:线程入口函数的参数,默认可以使用空

线程终止

#include <pthread.h>
//功能:线程终止(注意:进程终止是exit函数)
//函数原型
void pthread_exit(void *value_ptr);

//参数说明
参数value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

线程等待

#include <pthread.h>
//函数原型
int pthread_join(pthread_t thread, void **value_ptr);

//参数说明
参数thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

为什么需要线程等待?

1、已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
2、创建新的线程不会复用刚才退出线程的地址空间。
3、主线程需要知道所创建的新线程是否有完成任务,并且可以避免像僵尸进程的问题。

线程取消

#include <pthread.h>
//函数原型
int pthread_cancel(pthread_t thread);

//参数说明
参数thread:线程ID
返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。

thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。

如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED(这是一个宏定义,本质就是-1)。

如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给
pthread_exit的参数

如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

线程分离

int pthread_detach(pthread_t thread);

为什么需要使用线程分离

这是因为下面两点:

1、默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无
法释放资源,从而造成系统泄漏。

2、如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动
释放线程资源。

使用方法有下面两种:

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:pthread_detach(pthread_self());

使用效果:

线程分离以后,主线程以后就再也不需要关心这个线程了,在这个线程结束以后,会自动的进行资源释
放。但是这里有一点要注意:如果这个分离的线程出现了问题,依旧会导致进程的退出。

面向对象与基于对象

面向对象具有四大基本特征:抽象、封装、继承、多态,所以我们可以从面向对象的思想对线程进行封装。

面向对象的线程封装

有了抽象的概念之后,现在我们将线程抽象成一个类,用类的观点来看线程。

然后用线程类去创建具体的对象,线程对象与线程对象之间进行交互,通信。

接下来就为线程类设置成员,每个线程会有线程号,可以设置为数据成员;线程是否开始运行、线程是否结束,对应会有start函数与join函数;

线程入口函数可以封装为一个函数threadFunc,然后具体需要执行的任务可以封装为一个run方法,具体的业务留给Thread的派生类去实现,所以run方法可以设置为虚方法,Thread类也就成为抽象类了。

为了实现具体的任务方法,可以在用一个类去继承抽象类Thread,并实现虚方法run,所以可以设置为下面的
关系图:
在这里插入图片描述

代码实现

目录结构如下:

在这里插入图片描述

a.out是执行文件,TestThread.cc是测试文件,Thread.cc是实现文件,Thread.h是头文件。

Thread.h:

#ifndef  __Thread_H__

#define __Thread_H__

#include <pthread.h>

class Thread{
public:
	Thread();
	virtual ~Thread();

	//线程开始运行函数
	void start();
	//线程结束函数
	void join();

private:
	//线程入口函数被封装为一个threadFunc函数
	static void* threadFunc(void* arg);
	//具体执行的任务被封装为一个虚函数,由该类的派生类自行实现
	virtual void run() = 0;
private:
	//线程标识符
	pthread_t _thid;
	//是否在运行
	bool _isRunning;
};

#endif

Test.cc:

#include "Thread.h"
#include <pthread.h>
#include <stdio.h>

//实现构造函数
//就是对数据成员进行一个初始化
Thread::Thread():_thid(0),_isRunning(false){
	
}

//实现析构函数
Thread::~Thread(){
	//析构函数在这里本身可写也可不写,因为已经有了pthread_join函数来进行资源回收了
	//这里就写一下,万一在进程或者叫主线程退出时线程还在运行,那么就执行线程分离
	//线程分离的意思就是让子线程告知主线程不用管该子线程资源回收的事情了
	//即从主线程的控制下剥离出来独自被系统回收
	if(_isRunning){
		pthread_detach(_thid);
	}
}

//start函数创建线程
void Thread::start(){
	/*这里需要将threadFunc设置为静态的成员函数,不然会报错
	 * 因为对于pthread_create函数的第三个线程入口函数地址参数而言
	 * 其形式必须为void* (*funcName) (void*) 形式,即只能有一个void*类型的参数
	 * 而C++类中的成员函数参数列表的第一个位置都会隐含一个this指针,不符合上述形式
	 * 因此我们将threadFunc设置为静态的,消去this指针的存在
	 * */
	int ret = pthread_create(&_thid,nullptr,threadFunc,this);
	if(ret){
		perror("pthread_create");
		return;
	}
	//线程创建成功
	//那么成员数据_isRunning就该设置为true真在运行
	_isRunning = true;
}

//线程结束函数:主线程通过join来等待子线程的退出
void Thread::join(){
	//子线程还存在的话我们才执行资源回收
	if(_isRunning){
		pthread_join(_thid,nullptr);
		//线程停止后修改状态
		_isRunning = false;
	}
}

void* Thread::threadFunc(void* arg){
	//静态成员函数threadFunc无法直接调用非静态成员函数run
	//所以我们通过pthread_create函数的第四个参数传进this指针即可
	//因为这第四个参数就是线程入口函数threadFunc的参数
	Thread* t = static_cast<Thread*>(arg);
	if(t){
		//如果该指针不为空有值,那么就执行真正的线程任务
		t->run();
	}
	//执行结束后执行线程终止函数结束线程即可
	pthread_exit(nullptr);
}

TestThread.cc:

#include <iostream>
#include <unistd.h>
#include <memory>
#include "Thread.h"

using namespace std;

class MyThread:public Thread{
public:
	//MyThread类继承Thread类然后重写run方法
	void run() override{
		while(1){
			cout << "The thread is running" << endl;
			sleep(1);
		}
	}
};

//main就是主线程
int main(int argc,char** argv){
	//多态,基类类型型指针指向派生类类型对象
	//Thread* pthread = new MyThread(); 这样写时我们需要自己去回收这块堆空间
	//比较麻烦,所以我们使用智能指针让其自动回收
	unique_ptr<Thread> pthread(new MyThread());
	//使用start函数启动线程
	pthread->start();
	//启动之后使用join函数等待子线程结束进行资源的回收
	pthread->join();

	return 0;
}

运行结果:
在这里插入图片描述

基于对象的线程封装

解决问题的思维方式很多,除了之前C语言的面向过程的方法,以及上面的面向对象的方法之外,换一
种方式。我们前面学习过,类和类之家的关系除了继承,还有一些其他的关系,所以可以食欲组合与依
赖的方式,也就是基于对象的方法(不用使用继承)。因为没有继承,所以需要把Thread类中的抽象方
法run()函数通过function与bind的方式进行修改即可。之前的run方法是一个返回类型是void参数为空
的函数,现在直接使用bind改变函数的形态,将Thread变为一个非抽象类。然后将任务通过参数传给
Thread的构造函数,当然这个参数可以使用对应的数据成员进行接收,这个任务可以交给其他的类来完
成,并且打包给Thread线程进行执行,设计如下:
在这里插入图片描述

代码实现

目录结构同上,代码略有差异。

Thread.h:

#ifndef  __Thread_H__

#define __Thread_H__

#include <pthread.h>
#include <functional>

using namespace std;

//定义一个新的类型别名ThreadCllback,它代表一个没有参数(void ())和没有返回值(void)的可调用对象
//实际使用时我们可以这样使用:
//ThreadCallback myCallback = []{
//     一些代码
//};
//
//调用时直接:myCallback();
//就跟函数对象一样
using ThreadCallback = function<void ()>;

class Thread{
public:
	Thread(ThreadCallback&& cb);
	virtual ~Thread();

	//线程开始运行函数
	void start();
	//线程结束函数
	void join();

private:
	//线程入口函数被封装为一个threadFunc函数
	static void* threadFunc(void* arg);
private:
	//线程标识符
	pthread_t _thid;
	//是否在运行
	bool _isRunning;
	//要去实现的任务
	ThreadCallback _cb;
};

#endif

Thread.cc:

#include "Thread.h"
#include <pthread.h>
#include <stdio.h>

//实现构造函数
//就是对数据成员进行一个初始化
Thread::Thread(ThreadCallback&& cb):_thid(0),_isRunning(false),_cb(move(cb)){
	
}

//实现析构函数
Thread::~Thread(){
	//析构函数在这里本身可写也可不写,因为已经有了pthread_join函数来进行资源回收了
	//这里就写一下,万一在进程或者叫主线程退出时线程还在运行,那么就执行线程分离
	//线程分离的意思就是让子线程告知主线程不用管该子线程资源回收的事情了
	//即从主线程的控制下剥离出来独自被系统回收
	if(_isRunning){
		pthread_detach(_thid);
	}
}

//start函数创建线程
void Thread::start(){
	/*这里需要将threadFunc设置为静态的成员函数,不然会报错
	 * 因为对于pthread_create函数的第三个线程入口函数地址参数而言
	 * 其形式必须为void* (*funcName) (void*) 形式,即只能有一个void*类型的参数
	 * 而C++类中的成员函数参数列表的第一个位置都会隐含一个this指针,不符合上述形式
	 * 因此我们将threadFunc设置为静态的,消去this指针的存在
	 * */
	int ret = pthread_create(&_thid,nullptr,threadFunc,this);
	if(ret){
		perror("pthread_create");
		return;
	}
	//线程创建成功
	//那么成员数据_isRunning就该设置为true真在运行
	_isRunning = true;
}

//线程结束函数:主线程通过join来等待子线程的退出
void Thread::join(){
	//子线程还存在的话我们才执行资源回收
	if(_isRunning){
		pthread_join(_thid,nullptr);
		//线程停止后修改状态
		_isRunning = false;
	}
}

void* Thread::threadFunc(void* arg){
	Thread* t = static_cast<Thread*>(arg);
	if(t){
		//基于对象的回调函数执行线程任务
		t->_cb();
	}

	//执行结束后执行线程终止函数结束线程即可
	pthread_exit(nullptr);
}

TestThread.cc:

#include <iostream>
#include <unistd.h>
#include <memory>
#include "Thread.h"

using namespace std;

class MyThread{
public:
	void process(){
		while(1){
			cout << "The thread is running" << endl;
			sleep(1);
		}
	}
};

//main就是主线程
int main(int argc,char** argv){
	MyThread myThread;
	//基于Thread对象的形式执行我们需要的线程任务
	Thread th(bind(&MyThread::process,&myThread));
	th.start();
	th.join();
	return 0;
}

生产者与消费者问题

问题概述

在这里插入图片描述
如上图所示,所谓的生产者和消费者都是一个线程,生产者与消费者之间需要进行交互,生产者生产数据往缓冲区内放,生产一个放一个,消费者则会从仓库里面拿数据,此时这个缓冲区就是一个共享资源。

生产者与消费者在进行对缓冲区的访问时,为了使得线程同步所以需要获得锁才可以,获得了锁的线程才能进行访问,访问完之后再释放掉锁等其它线程竞争获取。

那为什么还需要条件变量呢?

来看这么个情况,如果消费者竞争到了锁,然后进入了缓冲区,但此时缓冲区内并没有数据(这是一个没有意义的操作),是不是就造成了锁资源的浪费?(拿到了锁却没有做到该线程应该做的事情,若是生产者拿到了就可以生产数据了呀,而且万一一直都是消费者拿到锁呢?)所以此时就需要条件变量的存在,条件变量的作用就是告诉该线程缓冲区内是否存在数据,若是有数据那么条件变量的条件成立,此时消费者线程池才会真正地去拿锁上锁进行缓冲区的访问。

生产者与消费者问题,本质就是多个线程的问题,但是会涉及到线程之间对共享资源的互斥访问,所
以需要有互斥锁与条件变量的准备知识,接下来我们先看看互斥锁与条件变量。

复习:互斥锁mutex

数据混乱的三个原因

1、资源共享(独享资源则不会出现混乱)
2、调度随机(意味着数据访问会出现竞争)
3、线程间缺乏必要的同步机制

总结:前面两个条件是不能改变的,因此若想让数据井然有序,只能改变第三个原因。

互斥锁的创建

pthread_mutex_t mutex;//直接定义一把互斥锁

互斥锁的初始化

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
						const pthread_mutexattr_t *restrict attr);
//功能: 初始化互斥锁
//参数解释
mutex:pthread_mutex_t类型的变量的地址,也就是互斥锁的名字。
mutexattr:pthread_mutexattr_t类型的变量地址,用于设置锁的属性,默认可以设为NULL;
返回: 成功0,错误返回错误代码。
静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:pthread_mutex_init(&mutex, nullptr);

互斥锁的销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

//功能: 销毁互斥锁
参数: mutex为要销毁的互斥锁的地址
返回: 成功0,错误返回错误代码

互斥锁的加锁

//这个操作是阻塞调用的,如果这个锁此时正在被其它线程占用,那么函数调用会进入到这个锁的排队队
//列中,并会进入阻塞状态,直到拿到锁之后才会返回
int pthread_mutex_lock(pthread_mutex_t *mutex);
//会尝试对互斥量加锁,如果该互斥量已经被锁住,函数调用失败,返回EBUSY,否则加锁成功返回0,
//线程不会被阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);

互斥锁的解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

复习:条件变量condition

条件变量的创建

pthread_cond_t cond;//定义一个条件变量 1

条件变量的初始化

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
						const pthread_condattr_t *restrict attr);
功能:初始化条件变量
cond:条件变量名的地址
cond_attr:条件变量的属性
返回: 成功0,错误返回错误编号
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化:pthread_cond_init(&cond, nullptr);

条件变量的销毁

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
cond:条件变量名的地址
返回: 成功0,错误返回错误代码

条件变量的等待

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
						pthread_mutex_t *restrict mutex);
功能:等待互斥锁mutex下的条件cond的发生
cond:要等待的条件变量
mutex:保护条件变量的互斥锁
返回:成功0,错误返回错误代码
//函数作用:
//1、阻塞等待条件变量cond满足。
//2、释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex); 第1,2步为一个原子操作
//3、当被唤醒,pthread_cond_wait函数返回,解除阻塞并重新获得互斥锁
pthread_mutex_lock(&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);
功能:发送条件变量cond
参数:cond为要发送的条件变量
返回:成功0,否则返回错误编号
备注:唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:发送条件变量cond
参数:cond为要发送的条件变量
返回:成功0,否则返回错误编号
备注: 通知所有等待该条件的线程

生产者消费者模型

建立模型

在这里插入图片描述
模型中的数据(即缓冲区中的数据)我们抽象为一个一个的int类型数据,push操作就是生产者线程往缓冲区内存放数据的行为,pop操作就是消费者线程从缓冲区中取走数据的行为。

仓库缓冲区我们采用图中 TaskQueue 任务队列的数据结构来进行设计,该数据结构的成员如上图所示,对应成员应该设计的比较好懂,不懂的话代码中会标有注释,这里不再赘述。

UML设计

在这里插入图片描述

代码实现

注意这里的 Thread.cc 和 Thread.h 依然是复用前文的面向对象的线程封装那一节的两个文件,此处不再赘述。

这里插一个vim上的批量行处理的快捷键方式:
在这里插入图片描述
目录如下:
在这里插入图片描述
Condition.h:

#ifndef __CONDITION_H__
#define __CONDITION_H__
#include <pthread.h>
#include "NoCopyable.h"

class MutexLock;//前向声明

class Condition : public NoCopyable{
	public:
		//因为每个条件变量会对应一把锁
		//所以需要在初始化一个条件变量时就将其对应的锁传进来
		Condition(MutexLock& mutex);
		~Condition();
		//阻塞
		void wait();
		//唤醒一个
		void notify();
		//广播唤醒
		void notifyAll();
	private:
		pthread_cond_t _cond;
		MutexLock& _mutex;
};


#endif

Condition.cc:

#include "Condition.h"
#include "MutexLock.h"
#include <pthread.h>

//因为每个条件变量会对应一把锁
//所以需要在初始化一个条件变量时就将其对应的锁传进来
Condition::Condition(MutexLock& mutex):_mutex(mutex){
	pthread_cond_init(&_cond,nullptr);
}

Condition::~Condition(){
	pthread_cond_destroy(&_cond);
}

//阻塞
void Condition::wait(){
	//pthread_cond_wait的第二个参数是锁的地址
	//但我们的锁是一个对象形式的MutexLock,其内部的成员变量_mutex
	//才是这里应该传进去的,但其是private私有的
	//因此我们应该到MutexLock类内写一个get函数,
	//其返回私有成员变量_mutex的地址
	pthread_cond_wait(&_cond,_mutex.getMutexLockPtr());
}

//唤醒一个
void Condition::notify(){
	pthread_cond_signal(&_cond);
}

//广播唤醒
void Condition::notifyAll(){
	pthread_cond_broadcast(&_cond);
}

MutexLock.h:

#ifndef __MUTEXLOCK_H__ //防止多次编译的问题

#define __MUTEXLOCK_H__

#include "NoCopyable.h"
#include <pthread.h>

class MutexLock:public NoCopyable{
	public:
		MutexLock();
		~MutexLock();
		//上锁
		void lock();
		//释放锁
		void unlock();
		//返回私有成员变量的地址
		pthread_mutex_t* getMutexLockPtr(){
			return &_mutex;
		}
	private:
		pthread_mutex_t _mutex;
};

class MutexLockGuard {
	public:
		MutexLockGuard(MutexLock& mutex)
		: _mutex(mutex)
		{
			_mutex.lock();
		}

		~MutexLockGuard(){
			_mutex.unlock();
		}
	private:
		MutexLock& _mutex;
};

#endif

MutexLock.cc:

#include "MutexLock.h"
#include <pthread.h>
#include <stdio.h>

//在构造函数中进行锁资源的初始化
MutexLock::MutexLock(){
	//锁初始化,第二个参数是锁的属性,默认为null
	int ret = pthread_mutex_init(&_mutex,nullptr);
	if(ret){
		perror("pthread_mutex_init");
	}
}

//在析构函数中销毁锁资源
MutexLock::~MutexLock(){
	int ret = pthread_mutex_destroy(&_mutex);
	if(ret){
		perror("pthread_mutex_destroy");
	}
}

//上锁
void MutexLock::lock(){
	int ret = pthread_mutex_lock(&_mutex);
	if(ret){
		perror("pthread_mutex_lock");
	}
}

//释放锁
void MutexLock::unlock(){
	int ret = pthread_mutex_unlock(&_mutex);
	if(ret){
		perror("pthread_mutex_unlock");
	}
}

TaskQueue.h:

#ifndef __TASKQUEUE_H__
#define __TASKQUEUE_H__
#include <queue>
#include "MutexLock.h"
#include "Condition.h"

class TaskQueue{
	public:
		TaskQueue(size_t queSize);
		~TaskQueue();
		bool empty() const;
		bool full() const;
		void push(const int &value);
		int pop();
	private:
		size_t _queSize;//缓冲区大小
		std::queue<int> _que; //缓冲区
		MutexLock _mutex; //互斥锁
		Condition _notEmpty; //是否空 的条件变量
		Condition _notFull; //是否满 的条件变量
};

#endif

TaskQueue.cc:

#include "TaskQueue.h"

TaskQueue::TaskQueue(size_t queSize)
:_queSize(queSize)
	,_que()
	,_mutex()
	,_notEmpty(_mutex)
	//这两个进行初始化时会使用该类对应的构造函数
	//所以需要传一个mutex参数
	,_notFull(_mutex)
{
	
}

TaskQueue::~TaskQueue(){
	
}

bool TaskQueue::empty() const{
	return _que.size() == 0;
}

bool TaskQueue::full() const{
	return _que.size() == _queSize;
}

void TaskQueue::push(const int &value){
	//在lock与unlock之间有可能出现return的情况
	//此时lock住的锁将无法被unlock导致死锁
	//而lock与unlock必须要成对出现,否则会有死锁问题
	//可以利用RAII的思想解决这种问题
	//_mutex.lock();
	//将上锁解锁操作放在MutexLockGuard类中通过构造器和析构函数的配合
	//可以自动上锁解锁,这是一种优化
	MutexLockGuard autoLock(_mutex);//autoLock栈对象
	//上锁之后,开始往缓冲区中写入数据
	//先判断缓冲区是否满
	//存在问题:当full为true的时候,_notFull会wait,假如wait被唤醒了
	//那程序就直接往下走了,很有可能被唤醒的同时full为false
	//因此使用if存在隐患,我们应该让线程被唤醒的时候依然判断一下full的值
	//所以使用while
	//if(full()){
	while(full()){//while循环可以解决虚假唤醒的概念
		//满了的话,生产者就陷入等待
		//等待的时候_mutex实际上就已经被释放掉了
		//wait操作实际上就是由两个部分组成的,这两个部分是原子操作
		//上半部分:阻塞线程直到条件变量cond满足
		//下半部分:释放已经拿到的互斥锁
		//当被唤醒时,pthread_cond_wait函数会返回,解除阻塞并重新获得互斥锁
		_notFull.wait();
	}
	//不满则开始放入数据
	_que.push(value);
	//既然有数据了,那么就可以唤醒因非空条件变量而阻塞的线程
	_notEmpty.notify();
	//_mutex.unlock();
}

int TaskQueue::pop(){
	//_mutex.lock();
	MutexLockGuard autoLock(_mutex);//autoLock栈对象
	//如果为空,消费者就陷入等待
	//if(empty()){
	while(empty()){
		_notEmpty.wait();
	}
	//tmp保存取出的值
	int tmp = _que.front();
	//让指针往下一个位置,即删掉一个队列头部的元素
	_que.pop();
	//此时因为被取走了一个数据,所以肯定是不满的
	//那么就可以唤醒因缓冲区满的条件变量而阻塞的线程
	_notFull.notify();
	//_mutex.unlock();

	return tmp;
}

NoCopyable.h:

#ifndef __NOCOPYABLE_H__
#define __NOCOPYABLE_H__

//继承了本类的成员都将具有对象语义
//即无法实现对象赋值和对象复制的操作
class NoCopyable{
	protected:
		NoCopyable(){

		}

		~NoCopyable(){

		}

		NoCopyable(const NoCopyable& rhs) = delete;
		NoCopyable& operator=(const NoCopyable& rhs) = delete;
};

#endif

Producer.h:

#ifndef __PRODUCER_H__
#define __PRODUCER_H__

#include "Thread.h"
#include "TaskQueue.h"
#include <stdlib.h>
#include <iostream>
#include <unistd.h>

class Producer:public Thread{
	public:
		Producer(TaskQueue& taskQueue):_taskQueue(taskQueue){

		}

		//真正的线程要执行的任务
		void run() override{
			int cnt = 10;
			//使用随机函数造数据
			//使用双冒号表示是C语言中的函数,这样有时候可以避免莫名其妙的错误
			//比如用C++写了一个和C函数同名的函数,用匿名空间表示就不会出问题
			::srand(clock());
			while(cnt-- > 0){
				int number = ::rand()%100;
				//将数据送进缓冲区中
				_taskQueue.push(number);
				std::cout << "producer >> number = " << number << std::endl;
				sleep(1);
			}
		}

		~Producer(){

		}
	private:
		TaskQueue& _taskQueue;
};

#endif

Consumer.h:

#ifndef __CONSUMER_H__
#define __CONSUMER_H__

#include "Thread.h"
#include "TaskQueue.h"
#include <stdlib.h>
#include <iostream>
#include <unistd.h>

class Consumer:public Thread{
	public:
		Consumer(TaskQueue& taskQueue):_taskQueue(taskQueue){

		}

		//真正的线程要执行的任务
		void run() override{
			int cnt = 10;
			//使用双冒号表示是C语言中的函数,这样有时候可以避免莫名其妙的错误
			//比如用C++写了一个和C函数同名的函数,用匿名空间表示就不会出问题
			//while(cnt-- > 0){
			while(1){
				//将数据从缓冲区中取出
				int number = _taskQueue.pop();
				std::cout << "consumer << number = " << number << std::endl;
				sleep(1);
			}
		}

		~Consumer(){

		}
	private:
		TaskQueue& _taskQueue;
};

#endif

TestPC.h:

#include "MutexLock.h"
#include "Producer.h"
#include "Consumer.h"
#include "TaskQueue.h"
#include <iostream>
#include <memory>

using namespace std;

int main(){
	TaskQueue taskQueue(5);
	unique_ptr<Thread> producer(new Producer(taskQueue));
	unique_ptr<Thread> producer2(new Producer(taskQueue));
	unique_ptr<Thread> consumer(new Consumer(taskQueue));
	
	producer->start();
	producer2->start();
	consumer->start();

	producer->join();
	producer2->join();
	consumer->join();

	//对象语义:不能进行复制或者赋值
	//值语义:可以进行赋值或者复制
	MutexLock mutex;
	//下面这种赋值或者构造都是可以的,但是没有意义
	//所以为了让下面这几行代码报错,让其具有对象语义
	//我们可以将其这俩函数设置为private私有的
	//或者设置为delete的,但是如果工程中类数以百计的话
	//都去一个一个的设置太麻烦了,这里可以使用继承的知识点
	//MutexLock mutex2 = mutex;//拷贝构造函数
	MutexLock mutex3;
	//mutex3 = mutex2;//赋值运算符函数
	return 0;
}

运行结果:
在这里插入图片描述

线程池

UML类图设计

在这里插入图片描述

UML时序图设计

代码实现

这个线程池的代码太多了,我直接贴出我的gitee链接了,感兴趣的可以自己去链接里看:线程池代码;