Linux相关概念和易错知识点(32)(互斥锁和条件变量的封装、日志和线程池的实现、策略模式)

C/C++ 面试大纲 c/c++面试大纲 阅读详情

目录

1.互斥锁的封装

(1)封装的目的

(2)实现代码

2.条件变量的封装

3.日志的实现

(1)刷新策略

①刷新到控制台

②刷新到文件

(2)构建日志信息

①刷新策略的选择

②生成日志信息

(3)宏观调用

①生成LogMessage临时对象

②宏函数简化操作

(4)全部代码

4.线程池的实现

(1)线程池的初始化

(2)线程启动

(3)激活线程

(4)释放线程池

(4)单例(懒汉)模式改进

(5)全部代码

①myThreadPool.hpp

②myThread.hpp


1.互斥锁的封装

我重点讲讲互斥锁的封装,条件变量的原理一致。

(1)封装的目的

如何封装都是小问题,毕竟互斥锁的相关函数也就那几个,我们重点要理解的就是为什么要封装,它和我们正常调用pthread_mutex系列函数有区别吗?回答了这个问题,我们自然对C++面向对象的特性有了更深的了解。

我们在封装之前,就是全局或局部定义一把锁,然后在需要的时候调用函数来加锁解锁。当我们需要达到某个功能时(如加锁解锁),我们会去直接调用实现这个功能最根本的方法,这就是面向过程设计。就像老师想关心学生成绩,就直接跑到班上去一个一个问。但万一记错了呢?跑错班了呢?这始终不方便。

当封装之后,就形成一个管理一把锁的类。什么叫管理一把锁的类?就是这个类的成员变量里有一把锁,用户不能直接操作这把锁,只能通过对外的接口来操作这把锁。假设定义了myMutex这个类,我需要一把锁,就生成个管理这把锁的结构体,锁就在这个结构体内。当这个线程需要加锁时,只需调用mutex.lock()即可。相当于将该线程的需求发送给了结构体,这个结构体负责实现细节。调用类的成员函数和其它代码的是同一个线程,因此能实现目的。就像老师就设计了一个学生管理系统,老师需要学生成绩就在系统上面点击获取成绩的需求,最后能得到成绩,至于使用细节不是这个老师关心的了。全程老师都面向系统来管理学生,老师和学生之间有一个中间层,这个中间层就是我们常说的面向对象编程的对象。

(2)实现代码

遵循上面的原则,我们可以得到如下代码

#pragma once

#include <iostream>
#include <pthread.h>

using namespace std;

namespace myMutexModule
{
	class myMutex
	{
	public:
		myMutex(const myMutex &) = delete;					// 锁不能被拷贝
		const myMutex &operator=(const myMutex &) = delete; // 锁不能被赋值

		myMutex()
		{
			pthread_mutex_init(&_mutex, nullptr); // 一个互斥锁已被创建并初始化
		}

		void lock()
		{
			pthread_mutex_lock(&_mutex); // 线程已加锁
		}

		void unlock()
		{
			pthread_mutex_unlock(&_mutex); // 线程已释放锁
		}

		pthread_mutex_t *Get_Lock_Ptr()
		{
			return &_mutex;
		}

		~myMutex()
		{
			pthread_mutex_destroy(&_mutex); // 一个互斥锁已被销毁
		}

	private:
		pthread_mutex_t _mutex; // 线程互斥的核心就是管理这把锁,mutex模块只需要描述好这把锁就可以了
	};

	// 当后续调用myLockGuard LockGuard(mutex)时(即创建myLockGuard对象时)会自动上锁,也会自动析构,这就是RAII思想,理解了智能指针一定能看懂这个类
	// 注意不能用匿名对象,因为临时对象的生命周期就一行,也就是说仅在一行内加锁解锁,没意义
	class myLockGuard
	{
	public:
		myLockGuard(myMutex &mymutex)
			: _mymutex(mymutex)
		{
			_mymutex.lock();
		}

		~myLockGuard()
		{
			_mymutex.unlock();
		}

	private:
		myMutex &_mymutex;
	};

}

2.条件变量的封装

实现代码如下,和互斥锁原理一模一样

#pragma once

#include <iostream>
#include <pthread.h>
#include "myMutex.hpp"

using namespace std;

namespace myCondModule
{
	class myCond
	{
	public:
		myCond(const myCond &) = delete; // 这个队列不能拷贝不能赋值
		const myCond &operator=(const myCond &) = delete;

		myCond()
		{
			pthread_cond_init(&_cond, nullptr); // 一个等待队列已被创建并初始化
		}

		void wait(myMutexModule::myMutex &mutex)
		{
			pthread_cond_wait(&_cond, mutex.Get_Lock_Ptr()); // 线程已加入等待队列,并自动释放锁
		}

		void notify()
		{
			pthread_cond_signal(&_cond); // 线程已移出等待队列,获得了锁并已开始向下执行代码
		}

		void notify_all()
		{
			pthread_cond_broadcast(&_cond); // 直接唤醒所有的锁
		}

		~myCond()
		{
			pthread_cond_destroy(&_cond); // 一个等待队列已被销毁
		}

	private:
		pthread_cond_t _cond; // 管理好这个队列就能实现同步,和互斥锁是一个原理的
	};

}

3.日志的实现

日志采用策略模式实现。

(1)刷新策略

打印日志的最后一步是要将日志刷新到文件或者显示器文件里,这部分我们用多态来实现。当后续我们拿着LogStrategy的指针或者引用时,我们调用SyncLog就能实现多态地打印。

①刷新到控制台

刷新到控制台和文件均需访问文件资源,那是临界资源,所以需要加锁来避免打印结果错乱。我这里使用的就是RAII思想的加锁,是基于前面互斥锁的封装来实现的。

②刷新到文件

构造时保证对应的log文件已经创建,default路径和文件名可自己定。

对文件IO的操作

调用说明:当用父类调用纯虚函数时,其实是根据虚函数表直接跑到了子类对象里面来调用函数,所以就算子类有自己的私有成员,也不会出现权限错误。

(2)构建日志信息

①刷新策略的选择

刷新策略只负责打印,其它的就交给构建日志信息的类处理,包括如何选择刷新策略

因此,调用log的不同成员函数就可以实现刷新策略的设置,这里利用了多态使得一个类型可以指向不同类型的对象。

②生成日志信息

实现了刷新策略的选择和打印,接下来就是最重要的日志信息生成。这必然是一个类来实现的,但这个类需要放在哪呢?以及我们还没有解决如何将刷新策略的选择和打印联系起来。

下面是一个关键代码

我们用LogMessage生成了信息之后,当这个对象析构的时候就会直接去调用刷新函数,也就是上面的进行策略选择的指针。

也就是说,我们的思路是用一个临时对象生成日志信息后,利用其析构函数将信息通过myLog的成员变量直接调用打印函数。因此,这个类最理想的位置就是作为myLog的内部类。因为实例化的时候可以直接传*this就能初始化LogMessage,能让LogMessage直接指向外部类的对象。

我们已经将刷新策略的选择和打印联系起来了。在LogMessage的构造函数中,我们就要让它自动生成信息。

很多零星的代码无关紧要,这里重要的是思路,最后我会贴出全部代码

很多时候我们在得到基本打印信息后还要多打印一些信息(如在程序中调用printf),这些信息通过重载来实现。

(3)宏观调用

细节思路我们已知,但宏观上这个日志到底是怎么个调用法我们还不清楚。

①生成LogMessage临时对象

LogMessage临时对象生成时会自动生成日志信息,当对象析构时会自动调用外部类的对象进而调用打印函数。因此调用日志打印就需要一个生成LogMessage的临时对象的函数,这个函数必然是在外部类myLog里。这和我们前面选择将LogMessage放在内部类的原因一致,我们直接传*this就能让LogMessage指向外部类了。

外部类本身就实例化出了一个myLog,因此调用log()可以以仿函数的形式打印日志对象。

至此,我们打印日志的外部代码就应该是log(level, file, line)这一行代码,通过这一行代码仿函数式生成LogMessage临时对象,对象析构时自动调用打印函数,一气呵成。

②宏函数简化操作

file和line最好用宏来替换。因此我们需要设计一些宏函数来快速调用

我们只需要调用LOG(level)就可以打印日志了。并且我们只需要使用CONSOLE_LOG()就能选择往显示器打印还是往文件打印,log是全局定义的,所以我们只需修改策略指针的指向即可。

最后结合LogMessage里面的operator<<,我们可以使用LOG(DEBUG) << "这是线程在执行代码"来打印需要的信息,并且前面附上日志信息。

(4)全部代码

注意需要引用"myMutex.hpp"头文件

#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <fstream>
#include <cstring>
#include <sstream>
#include <time.h>
#include <pthread.h>
#include <memory>
#include <filesystem> //C++的文件操作,c++17的新特性
#include "myMutex.hpp"

using namespace std;

// 日志指标:
// 时间戳、日志等级、日志内容、文件名及其行号(可选)、进程线程相关id信息等(可选)
namespace myLogModule // 写日志采用策略模式,多种刷新策略
{

	// 日志默认保存路径
	const string default_log_path = "./log/";
	const string default_log_name = "log.txt";

	// 日志等级
	enum LogLevel // 普通枚举,可直接用
	{
		DEBUG,	 // 调试信息
		INFO,	 // 正常信息
		WARNING, // 警告
		ERROR,	 // 错误
		FATAL	 // 致命错误
	};

	string Level2String(LogLevel level)
	{
		// 直接枚举转换
		switch (level)
		{
		case LogLevel::DEBUG:
			return "DEBUG";
		case LogLevel::INFO:
			return "INFO";
		case LogLevel::WARNING:
			return "WARNING";
		case LogLevel::ERROR:
			return "ERROR";
		case LogLevel::FATAL:
			return "FATAL";
		default:
			return "NONE";
		}
	}

	// 刷新策略,向不同文件中刷新,也就是说在获取到日志要打印的所有信息后调用这些类里面的方法实现不同打印
	class LogStrategy // 多态,父类
	{
	public:
		virtual void SyncLog(const string &message) = 0; // 纯虚函数,用多态实现不同策略
	};

	// 刷新到控制台
	class ConsoleLogStrategy : public LogStrategy
	{
	public:
		void SyncLog(const string &message)
		{
			myMutexModule::myLockGuard LockGuard(_mutex);
			printf("%s\n", message.c_str()); // 自动换行
		}

	private:
		myMutexModule::myMutex _mutex;
	};

	// 刷新到文件中
	class FileLogStrategy : public LogStrategy
	{
	public:
		FileLogStrategy(string log_path = default_log_path, string log_name = default_log_name)
			: _log_path(log_path), _log_name(log_name)
		{
			// 是否存在文件路径
			if (filesystem::exists(_log_path)) // C++17的管理文件的特性,需要的时候再查就行,不需要全部记下来
				return;

			// 临界资源,有可能多个线程来访问,会一块创建文件,所以要用锁
			myMutexModule::myLockGuard LockGuard(_mutex);
			try
			{
				// 不存在就新建,用try
				filesystem::create_directories(_log_path);
			}
			catch (filesystem::filesystem_error &e) // create_directories抛出异常的类型
			{
				// 用cerr打印,提高易读性
				cerr << e.what() << endl; // 库里抛出的异常可用what()来解读
			}
		}

		// c++文件操作
		void SyncLog(const string &message)
		{
			// 保证一次只有一个线程进入执行
			myMutexModule::myLockGuard LockGuard(_mutex);

			// c++IO流文件操作,以app形式打开
			string log = _log_path + _log_name;
			ofstream fout(log, ios::app);

			// 判断文件是否打开
			if (!fout.is_open())
			{
				printf("文件打开失败\n");
				return;
			}

			// 向文件里面写
			fout << message << endl;

			// 关闭文件
			fout.close();
		}

	private:
		string _log_path;
		string _log_name;

		// 锁,防止重复新建文件夹
		myMutexModule::myMutex _mutex;
	};

	// 构建刷新函数的message,日志消息的形成
	class myLog
	{
	public:
		myLog()
		{
			// 默认控制台策略
			// 这是智能指针实现的多态,父类指针管理子类对象
			_strategy = make_shared<ConsoleLogStrategy>(); // 构建shared_ptr类型
		}

		// 调用父类的函数就可实现多态
		void myConsoleLog()
		{
			// 这是智能指针实现的多态,父类指针管理子类对象
			_strategy = make_shared<ConsoleLogStrategy>(); // 构建shared_ptr类型
		}

		void myFileLog()
		{
			// 这是智能指针实现的多态,父类指针管理子类对象
			_strategy = make_shared<FileLogStrategy>(); // 构建shared_ptr类型,这里无需传参,使用默认路径
		}

		// 日志指标:
		// 具体时间、日志等级、日志内容、文件名及其行号(可选)、进程线程相关id信息等(可选)
		// 一个LogMessage包含所有日志信息
		class LogMessage
		{
		public:
			string GetCurTime()
			{
				time_t time_stamp = time(nullptr); // 返回的是时间戳,但要转为具体时间

				tm cur_time; // 库的一个类型,里面有年月日等信息,可以用时间戳来转换
				//_r表示函数可重入,可多个函数进来获取
				localtime_r(&time_stamp, &cur_time); // 将时间戳转为具体时间

				// 形成完整的时间
				char time_message[1024] = {0};
				snprintf(time_message, sizeof(time_message),
						 "%4d-%02d-%02d %02d:%02d:%02d",
						 cur_time.tm_year + 1900,
						 cur_time.tm_mon + 1,
						 cur_time.tm_mday,
						 cur_time.tm_hour,
						 cur_time.tm_min,
						 cur_time.tm_sec);

				return time_message; // 自动构造
			}

			// 构造函数生成日志信息
			LogMessage(LogLevel level, const string &file_name, uint32_t line, myLog &cur_log)
				: _cur_time(GetCurTime()), _level(level), _pid(getpid()), _file_name(file_name), _line(line), _cur_log(cur_log)
			{
				stringstream ssmessage;
				ssmessage << "[" << _cur_time << "] "			 // 具体时间
						  << "[" << Level2String(_level) << "] " // 日志等级
						  << "[" << _pid << "] "				 // 进程pid
						  << "[" << _file_name << "] "			 // 文件名
						  << "[" << _line << "] - ";			 // 日志所在的代码的行号

				// 获取string
				_message = ssmessage.str();
			}

			// 日志信息还能在基本信息后面田间,因此要支持<<,每层<<后返回值都是LogMessage本身的引用
			template <typename T>
			LogMessage &operator<<(const T &data)
			{
				stringstream ssmessage;
				ssmessage << data;
				_message += ssmessage.str();
				return *this; // 多个<<
			}

			~LogMessage()
			{
				if (_cur_log._strategy)
				{
					// 自动按照指定方式输出日志信息
					_cur_log._strategy->SyncLog(_message);
				}
			}

		private:			   // 管理日志信息的生成
			string _cur_time;  // 具体时间
			LogLevel _level;   // 日志等级
			pid_t _pid;		   // 进程pid
			string _file_name; // 文件名
			uint32_t _line;	   // 日志所在的代码的行号

			string _message; // 上述所有信息总和,完整的一条日志信息

			myLog &_cur_log; // 负责不同的策略进行刷新
		};

		// 用来进行LogMessage的快速初始化
		// 这相当于提供了一种仿函数,用类名+(参数)
		LogMessage operator()(LogLevel level, const string &file_name, uint32_t line)
		{
			// 临时对象,值返回,一行代码结束后可以自动调用它的析构,打印日志信息
			return LogMessage(level, file_name, line, *this); // 这里就拿到了外部类的策略了
		}

	private:
		shared_ptr<LogStrategy> _strategy; // 日志刷新的方案,用顶层父类来管理
	};

	myLog log;

// 利用宏函数实现快速打印
// log(level, __FILE__, __LINE)本质是仿函数,调用的operator()
#define LOG(level) log(level, __FILE__, __LINE__) // 获取调用该代码的行号
												  //__FILE__, __LINE是预处理符,是宏替换,替换成文件名和行号

	// 注意宏定义函数末尾不能加分号
#define CONSOLE_LOG() log.myConsoleLog()
#define FILE_LOG() log.myFileLog()

}

4.线程池的实现

线程池的本质就是生产者消费者模型,我们最终的目标是实现如下线程池调用,用一个结构体来管理里面所有的线程,当添加任务时线程就会去分配执行任务,执行完后就等着新任务的加入。最后停止添加任务后再回收所有线程。

(1)线程池的初始化

<T>指的是任务类型,即上一张图的task_t

这里我使用绑定将this指针给隐藏了,使得myThread能够处理这样的任务。我们需要注意到这些线程即将执行的代码都是同一个,这意味着这个consumer_pop一定不是任务

push_back是将即将生成的线程全部通过myThread管理起来,并将myThread管理起来。这些线程接下来会执行的函数就是void(string)类型的。

(2)线程启动

启动的时候修改_isrunning标识,这个标志着线程池是否启动。当调用这个函数的时候,每个线程都会去开始执行绑定的void(string)的函数,这个函数是任务吗?并不是!

下面是所有函数被激活进入函数的代码:

	// 这里对应生产消费模型的消费场所,各个线程进入这个函数来获取任务
		// 每个线程启动后都会执行这个函数,这个函数的执行是交给myThread实现的
		void consumer_pop(string &name) // 消费
		{
			while (1) // 重复进入,直到任务为空,任务为空后都会到等待队列里面
			{
				T task;
				{
					myMutexModule::myLockGuard LockGuard(_mutex); // 上锁,一次一个线程进入等待队列

					while (isTaskEmpty() && _isrunning)
					{
						_wait_consumer++;
						_consumer_cond.wait(_mutex); // 消费者进入等待队列
						_wait_consumer--;
					}

					// 不在运行状态的话,就退出获取任务的代码
					if (isTaskEmpty() && !_isrunning)
						break;

					// 获取任务,将任务pop
					task = _task_queue.front();
					_task_queue.pop();
				}
				// 在这里锁就释放了,获得任务的线程自己执行下面的代码,把锁还回去给其它线程用

				task(name); // 该线程执行它得到的任务,这个任务所需要的参数就需要线程对应的结构体来维护了
			}
		}

所有线程一开始进来之后,由于里面没有任务且_isrunning标识为true,所以所有的线程都会被陷入等待队列中,_wait_consumer用来统计有多少个正在等待的线程。并且我们需要注意就算线程没有被阻塞在等待队列,它们也一定会在这个函数里面死循环,有且仅有_isrunning为false且任务执行完了才会跳出。这是后面销毁线程池的必要条件。

(3)激活线程

每添加一个任务,就会尝试唤醒线程执行代码,直到所有线程都在执行中。同时如果多个线程同时进入的话,很有可能push出现错误,导致被多添加任务,所以要加锁。加锁与否要充分考虑多线程的情况,就算当前不会以多线程来调用这个函数。

(4)释放线程池

这里将标志位改了之后且所有线程执行完任务后,就会从函数中break,可以紧接着释放线程。

(4)单例(懒汉)模式改进

删掉拷贝和赋值,通过一个static指针来提供对象

只有第一次单例指针没有值时才会创建对象,第二次调用就会直接访问已创建好的单例,不会生成新的对象了。

同时这里的代码不能被多个线程访问,这样就不是单例了,所以要进行加锁处理。加锁后最外层又套了一层if,这是为了防止每次调用这个函数都要加锁,会影响速度。

下面是运行结果的参考

(5)全部代码

①myThreadPool.hpp


// 线程池原理:
// 预创建一个线程池,内部维护多个线程,有任务的时候再叫醒线程
// 线程个数可以控制(浮动线程池),但不宜大量波动,保证线程稳定

// 固定线程池

#include <queue>
#include <vector>
#include <functional>
#include "myThread.hpp"
#include "myLog.hpp"
#include "myMutex.hpp"
#include "myCond.hpp"

using placeholders::_1;

using task_t = function<void(string)>;

using namespace myCondModule;
using namespace myMutexModule;
using namespace myLogModule;
using namespace myThreadModule;

myMutexModule::myMutex _mutex_fun; // 只用来保护函数
int fun(string name)
{
	myMutexModule::myLockGuard LockGuard(_mutex_fun); // 上锁,一次一个线程进入等待队列
	static int count = 0;
	LOG(DEBUG) << "[" << name << "]" << "线程在执行任务" << ",该任务共总执行了" << ++count << "次";
	return 0;
}

namespace myThreadPoolModule
{

	// 线程个数,最好为物理CPU个数 * 核数,即一个8核的CPU运行一个程序推荐线程数为8,部分可超线程的机器可设置16等核数的倍数个线程
	const int default_thread_num = 2;
	// 智能指针,指向管理线程的结构体

	// 指定void()的函数,void fun()
	using thread_t = shared_ptr<myThread>;
	// thread_t是管理即将执行void()函数的线程的结构体指针

	// 模板参数T代表任务类型
	template <typename T>
	// 懒汉模式
	class myThreadPool
	{
		// 拷贝构造、赋值重载禁掉
		myThreadPool(const myThreadPool<T> &mtp) = delete;
		myThreadPool<T> &operator=(const myThreadPool<T> &mtp) = delete;
		// 只允许调用构造

	private:
		bool isTaskEmpty()
		{
			return _task_queue.empty();
		}

		// 这里对应生产消费模型的消费场所,各个线程进入这个函数来获取任务
		// 每个线程启动后都会执行这个函数,这个函数的执行是交给myThread实现的
		void consumer_pop(string &name) // 消费
		{
			while (1) // 重复进入,直到任务为空,任务为空后都会到等待队列里面
			{
				T task;
				{
					myMutexModule::myLockGuard LockGuard(_mutex); // 上锁,一次一个线程进入等待队列

					while (isTaskEmpty() && _isrunning)
					{
						_wait_consumer++;
						_consumer_cond.wait(_mutex); // 消费者进入等待队列
						_wait_consumer--;
					}

					// 不在运行状态的话,就退出获取任务的代码
					if (isTaskEmpty() && !_isrunning)
						break;

					// 获取任务,将任务pop
					task = _task_queue.front();
					_task_queue.pop();
				}
				// 在这里锁就释放了,获得任务的线程自己执行下面的代码,把锁还回去给其它线程用

				task(name); // 该线程执行它得到的任务,这个任务所需要的参数就需要线程对应的结构体来维护了
			}
		}

	public:
		myThreadPool(int thread_num = default_thread_num)
			: _thread_num(thread_num), _isrunning(false), _wait_consumer(0)
		{
			for (int i = 0; i < _thread_num; i++)
			{
				// 利用绑定先传this
				_thread_pool.push_back(make_shared<myThread>(bind(&myThreadPool::consumer_pop, this, _1)));
				// this占位,实际上线程执行的函数类型是void(string)
				LOG(INFO) << "构建线程" << _thread_pool.back()->GetName() << "成功";
			}
		}

		// 静态成员函数,直接调用这个函数可以直接获得myThreadPool<T>*
		// 要保护线程池不被再次调用,多线程调用会出现问题
		static myThreadPool<T> *GetInstance()
		{
			// 如果存在单例了,再次调用就会直接得到这个单例,不会第二次生成
			if (_instance == nullptr) // 多加一层判断,可以保证第二次调用这个函数不会加锁
			{
				myMutexModule::myLockGuard LockGuard(_mutex_instance); // 防止多线程调用
				if (_instance == nullptr)
				{
					LOG(INFO) << "线程池创建准备中";
					_instance = new myThreadPool<T>();
					LOG(INFO) << "已完全加载线程池";
				}
			}

			// 调用这个函数,返回的是单例的指针
			return _instance;
		}

		void AddTask(T &&task)
		{
			// 一次只允许一个线程进入
			myMutexModule::myLockGuard LockGuard(_mutex);
			if (!_isrunning)
				return;
			_task_queue.push(move(task));
			if (_wait_consumer)			 // 有任务就唤醒线程,在工作状态下一直处理任务
				_consumer_cond.notify(); // 线程等待队列没有闲置的线程,就不走这行代码
		}

		void StartAddTask()
		{
			myMutexModule::myLockGuard LockGuard(_mutex); // 避免is_running重复访问
			if (_isrunning)
				return;

			_isrunning = true; // 线程可以开始运行了

			for (auto &thread_ptr : _thread_pool)
			{
				thread_ptr->start(); // 所有线程全部陷入consumer_pop中
				LOG(INFO) << "启动线程" << thread_ptr->GetName() << "成功";
			}
		}

		void join_threads()
		{
			for (auto &thread_ptr : _thread_pool)
			{
				thread_ptr->join();
				LOG(INFO) << "释放线程" << thread_ptr->GetName() << "成功";
			}
		}

		void StopAddTask()
		{
			myMutexModule::myLockGuard LockGuard(_mutex);
			if (_isrunning) // 避免重复访问,加锁
			{
				// 不能直接thread_ptr->cancel(),否则任务状态不确定
				_isrunning = false; // 不能再AddTask
				if (_wait_consumer)
					_consumer_cond.notify_all(); // 唤醒所有线程去执行剩余任务
			}
		}

	private:
		myMutexModule::myMutex _mutex;

		int _thread_num;			   // 线程池的线程数
		queue<T> _task_queue;		   // 放任务
		vector<thread_t> _thread_pool; // 存管理线程的结构体指针

		bool _isrunning; // 线程池是否正在工作状态,不在工作状态的话保存线程但不会执行函数

		myCondModule::myCond _consumer_cond; // 消费者的等待队列
		int _wait_consumer;					 // 正在等待的消费者数量

		static myThreadPool<T> *_instance;			   // 单例对应的指针
		static myMutexModule::myMutex _mutex_instance; // 用来保护单例的锁
	};

	// 类里面出现的静态成员初始化要在类外,不加static
	template <typename T>								   // 变量名前要写对应的类域
	myThreadPool<T> *myThreadPool<T>::_instance = nullptr; // 单例的指针初始化
	template <typename T>
	myMutexModule::myMutex myThreadPool<T>::_mutex_instance; // 只用来保护单例
}

②myThread.hpp

#pragma once

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>

using namespace std;

namespace myThreadModule
{
    static int count = 1;

    enum class STATE // 强枚举类型,使用里面的成员需要写成STATE::RUN,普通枚举可以直接写成RUN,也能识别
    {
        READY,
        RUN,
        FINISH,
        CANCEL,
        DETACH
    };

    class myThread
    {
        using func_t = function<void(string &)>; // 传的函数类型规定为void fun(T parameter)

        static void *execute(void *p) // this指针
        {
            static_cast<myThread *>(p)->_fun(static_cast<myThread *>(p)->_name); // 还原this指针类型,并且使它执行对应对象的函数
            return nullptr;
        }

    public:
        myThread(func_t fun)
            : _tid(-1), _state(STATE::READY), _fun(fun), _joinable(true)
        {
            _name = "myThread-" + to_string(count++);
        }

        bool start()
        {
            if (_state == STATE::READY)
            {
                pthread_create(&_tid, nullptr, execute, (void *)this); // 执行的函数类型和void*(void*)不同,需要转接一次
                _state = STATE::RUN;                                   // 修改线程的状态
                return true;
            }
            printf("%s线程start失败\n", _name.c_str());
            return false;
        }

        bool join()
        {
            if (_joinable)
            {
                if (pthread_join(_tid, nullptr) == 0)
                {
                    _joinable = false; // 已回收资源,不能再join
                    _state = STATE::FINISH;
                    return true;
                }
            }
            printf("%s线程join失败\n", _name.c_str());
            return false;
        }

        bool cancel()
        {
            if (_state == STATE::RUN)
            {
                if (pthread_cancel(_tid) == 0)
                {
                    _state = STATE::CANCEL;
                    return true;
                }
            }
            printf("%s线程cancel失败\n", _name.c_str());
            return false;
        }

        bool detach()
        {
            if (_state == STATE::RUN)
            {
                if (pthread_detach(_tid) == 0)
                {
                    _joinable = false; // detach后就不能join了
                    _state = STATE::DETACH;
                    return true;
                }
            }
            printf("%s线程detach失败\n", _name.c_str());
            return false;
        }

        string GetName()
        {
            return _name;
        }

    private:            // 管理线程,只需要描述好这个线程即可
        pthread_t _tid; // 线程的id
        string _name;   // 线程的名字
        STATE _state;   // 线程的状态
        func_t _fun;    // 线程要执行的函数
        bool _joinable; // 这个线程是否能够被join
    };

}

C++构建高性能网络安全测试工具:架构设计与核心实现 网络协议与系统编程是构建高性能安全工具的技术基石。理解TCP/IP协议栈、操作系统进程与内存管理、多线程并发等底层原理,是实现高效网络通信与资源调度的关键。这些技术通过优化I/O模型、减少数据拷贝和降低锁竞争,显著提升工具在端口扫描、协议分析等场景下的性能与稳定性。在网络安全领域,掌握这些核心能力对于开发用于授权测试与漏洞研究的模拟工具至关重要。本文以C++实现为例,深入探讨如何运用线程池、异步I/O和模糊测试框架,构建一个模块化、可扩展的网络安全测试平台,满足安全研究中对性能与可靠性的严苛要求。 阅读详情

相关推荐

C++ 知识补充

向量(vector)是一个封装了动态大小数组的顺序容器(SequenceContainer)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。C++中map提供的是一种键值对容器,里面的数据都是成对出现的,如下图:每一对中的第一个值称之为关键字(key),每个关键字只能在map中出现一次;第二个称之为该关键字的对应值。

weixin_47059239的博客 2130

【Linux】三十.线程篇七《手写线程池 和日志+ 策略模式完整实战、线程安全的单例模式、STL+智能指针(万字解析)》

IT⾏业这么⽕, 涌⼊的⼈很多. 俗话说林⼦⼤了啥⻦都有. ⼤佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖⼤佬的后腿, 于是⼤佬们针对⼀些经典的常⻅的场景, 给定了⼀些对应的解决⽅案, 这个就是 设计模式。

2501_93351213的博客 888

中科闻歌Java面试题及参考答案

Java 集合框架整体分为 Collection 与 Map 两大体系,Collection 存储独立单个元素,Map 存储键值对映射关系,面试答题关键点在于区分接口和具体实现类,不能只罗列类名,需要从底层存储结构出发推导性能、特性、线程安全情况,同时区分过时实现类,这部分属于面试加分点,很多面试人员只会背诵类名,忽略底层结构与线程安全带来的取舍。Collection 下分为 List、Set、Queue 三个子接口。List 集合特征为元素有序、允许元素重复,存入顺序和遍历取出顺序保持一致。ArrayLi

大模型大数据攻城狮的专栏 370

linux笔记归纳12:线程同步与互斥

一个全局资源,如果没加保护,可能会有并发问题,又称线程安全问题如果票数是指针,或者是控制线程运行的标记位,都会引发相应的问题route函数被多个执行流重复进入,但有全局变量资源,是不可重入函数。

yonakaame的博客 249

【信息科学与工程学】计算机科学与自动化 第二百零一十一 软件开发模型01

编号类型模型领域模型中的知识点/因素/参数关联知识和法律法规1传统顺序模型瀑布模型 (Waterfall)通用软件开发、硬件紧密耦合系统、政府/军工项目、需求稳定的传统信息系统严格线性阶段:可行性研究→需求分析(SRS,含功能/非功能/接口/约束,使用IEEE 830模板)→系统设计(概要设计HLD:架构、模块划分、数据库ER图;详细设计LLD:伪代码、类图、接口定义)→实现(编码规范、代码审查、静态分析工具SonarQube)→测试(单元测试JUnit、集成测试、系统测试、验收测试UAT,测试计划提前编写

weixin_49199313的博客 149

一步步教你离线安装vsftpd并配置FTP服务

在安装vsftpd之前,需确保系统更新至最新版本,并检查防火墙配置以允许FTP流量。安装过程涉及使用包管理器(如apt-get或yum)下载vsftpd及其依赖包,特别是在离线环境中,需先下载所有相关包并传输到目标机器。安装完成后,通过命令验证安装并启动服务。配置vsftpd包括编辑配置文件以设置访问权限和安全性参数,如匿名访问和本地用户访问。此外,需配置防火墙开放FTP端口,并通过FTP客户端测试服务。最后,建议进行安全配置,如禁用匿名访问和配置SSL/TLS加密,并设置日志记录以监控服务状态。

a_join的博客 2483

面试问题合集

回答结构(STAR法则)“我介绍一个智能门禁系统的项目。项目背景:这是一个基于Qt和OpenCV的跨平台门禁系统,解决小区/办公室的访客管理和门禁授权问题。我的职责:我独立完成了人脸识别模块和数据库模块。技术实现:人脸识别用OpenCV的Haar Cascade做检测,然后通过灰度化、缩放到32x32、归一化提取特征向量,匹配时用余弦相似度。数据库用SQLite,我用Repository模式封装了数据访问。重难点:性能优化方面,我把视频处理放到子线程避免卡界面;

丰锋的博客 240

三电平逆变器LC滤波设计避坑指南:从SVPWM谐波到Simulink仿真验证

本文详细解析了三电平逆变器LC滤波设计中的关键问题,从SVPWM谐波特性到Simulink仿真验证,提供了800V/10kW系统的参数计算方法和优化技巧。通过实际案例和仿真配置示例,帮助工程师避免常见设计误区,提升电能质量和系统稳定性。

weixin_29261711的博客 173

linux笔记归纳14:Socket编程UDP

作用:创建一个通信的一端参数1:域本地通信:AF_UNIX网络通信:AF_INET参数2:套接字类型UDP:SOCK_DGRAM(面向数据报)TCP:SOCK_STREAM(面向字节流)参数3:设置为0返回成功:文件描述符返回失败:-1。

yonakaame的博客 326

Minitab实战:用Weibull分布预测产品寿命的5个关键参数(附真实案例)

本文通过一个真实的电容器老化测试案例,详细介绍了如何运用Minitab软件进行Weibull分布分析,以预测产品寿命。文章深入解读了形状参数β、尺度参数η等五个关键参数的工程意义与商业价值,指导工程师从失效数据中提取洞察,并将其应用于可靠性评估、保修策略制定及成本预估等实际业务决策。

weixin_29231263的博客 520

cadence数模混合仿真反标sdf

数模混合IC设计时经常要调用一些Verilog模块(最简单的例如SPI接口),在前仿时一般直接用rtl代码来仿真,但是后仿时要关注因寄生带来的延迟,避免这些额外的延迟导致时序错误,因此不能简单地采用RTL来仿真。 一般有两种办法:1是采用由RTL综合得到的门级电路来仿真,这种方法比较简单,但是缺点是规模较大,而且只包括门延迟不包括因版图导致的线延迟;如果想要仿真更精细,就要对该这部分门电路提取寄生参数; 另一种方式是利用sdf文件(standard delay format)来进行仿真。后端工具在布局布

长河柱天的博客 7274

147.【2024Java八股-全网最全-10w字】

eg: 学校、院校、经验、年龄、跳槽频率…基本包括16种常见使用场景: 、数据共享分布式、、全局 ID、计数器、限流、位统计、购物车、用户消息时间线 timeline、、抽奖、点赞、签到、打卡、商品标签、商品筛选、用户关注、推荐模型、排行榜.详细讲解16种使用场景: https://blog.youkuaiyun.com/zxl646801924/article/details/123202381用户使用id进行查询文章的时候,首先会查询redis数据库,如果在redis中命中数据,那么redis就向用户返回查询的结果

qq_69683957的博客 1736

【第48例】BLM模型之市场洞察-五看工具篇(一)

BLM市场洞察

产品线负责人 2109

java八股文极速版

时间和空间的权衡 值越高空间开销小但是增加了查找开销服务于从Key映射到index的Hash算法,在性能和内存的使用上取平衡,实现一个尽量均匀分布的Hash函数,选取16,是通过位运算的方法进行求取的。线程安全:当多线程执行同一段程序的时候,如果发生了和预期结果不一致的情况,就是线程不安全的,如果和预期结果一致就是线程安全的,可以加锁解决(把并行运行的线程变成串行化执行)。1.加锁 利用Synchronized或者ReenTrantLock来对不安全对象进行加锁。

PassionAnytime的博客 382

YOLOv8性能突破:DySample超轻量动态上采样算子的创新应用与实现【附保姆级代码】

YOLOv8(You Only Look Once Version 8)作为YOLO系列模型的最新版本,延续了高效、准确的目标检测优势。其基础结构包含了骨干网络(Backbone)、检测头(Head)、以及特征融合层(Neck)。在特征融合阶段,YOLOv8需要对不同尺度的特征图进行上采样,以便将低分辨率的特征与高分辨率的特征进行融合。传统的上采样操作多使用插值(如双线性插值)或固定卷积,而这些方法可能无法很好地适应复杂的上下文信息。

一键难忘的博客 3120

电商平台系统需求规格说明书(示例)

​​电商系统​​:指本文件所描述的集商品展示、销售、支付、物流跟踪、用户管理等功能于一体的综合性在线购物平台。​​API​​:应用程序编程接口(Application Programming Interface),用于实现系统各模块之间或与外部系统之间的数据交互和功能调用。

跟佟格湾一起学习计算机知识吧~ 1262

知识总结 - 常见问题

1. InnoDB存储引擎,三层B+树,单表能存储多少数据MySQl存储单元为1页,1页为16K,即16384Byte非叶子节点存储单元为主键+指针,大小为8+6=14Byte单页可存储16384/14 ≈ 1170个单元,即可存储1170个子节点每个存储单元可标记1页,则前两层可标记1170x1170页第三层叶子节点存储数据,每条数据约1K,单页则可存储16/1=16个数据三层 B+树,单表可存数据约为1170x1170x16=21902400条数据。

yuyehui的博客 834

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

深入C++与QT高频面试题(不定时更新)

本文总结了C++、数据库和Qt相关的常见面试题,涵盖以下核心内容: C++特性与智能指针 智能指针(unique_ptr/shared_ptr/weak_ptr)解决内存管理问题 构造函数/析构函数特性,虚函数与纯虚函数区别 STL容器分类与特性(vector/list/set/map等) 多线程编程 线程创建方式(Thread类/moveToThread) 线程同步机制(互斥锁/条件变量/读写锁) 进程间通信方式(管道/共享内存/消息队列) 数据库基础 事务ACID特性与操作流程 索引原理与使用场景 关系

阿博的博客 1255

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Java秋招八股之王——拿不到大厂Offer来找我(全文14w字,持续更新)

如果你需要一套八股文,想要一套八股文解决秋招的大部分问题,来看看这篇文章吧~内容包含数据结构、计算机网络、数据库、Redis、消息队列、JavaSE、JVM、JUC、海量数据题目、高并发场景、项目的亮点难点、ssm框架、dubbo框架等等。这是我整个秋招不停面试的总结和反思,最后也是靠它拿到了菜鸟的offer,希望会对后面秋招的朋友起到一些帮助。

qq_40460454的博客 1291

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

互联网校招笔试题型全拆解:测试开发与后端核心考点与实战策略

校招笔试是进入互联网大厂的关键环节,其题型设计往往围绕计算机基础、逻辑思维与工程实践能力展开。从TCP握手、进程线程到MySQL索引与Redis缓存,基础知识的掌握程度直接决定笔试成绩。针对测试开发岗位,用例设计、自动化框架、性能测试是三大核心考点,需要掌握等价类划分、分层架构与指标分析等工程化方法;后端岗位则聚焦Java并发、Spring原理、数据库事务及分布式锁等高频问题。开放设计题如短链系统、Feed流方案,更看重需求拆解与技术选型的权衡能力。合理分配90分钟答题时间,并通过错题知识树复盘,能够有效提

weixin_33924312的博客 319

秋招笔记——汇总

今天把所有的秋招笔记汇总在这里,不然我的个人主页太杂乱了,这样也方便复习。

lastXuanGod的博客 304

要复习的那些点

2021年离职计划:30岁之前,还是期望在大厂工作,期望薪资是50万/年、涨幅在30%,工作地点在杭州 时间 动作 2021:02:01 离职原因(利弊权衡): 目前现状: 部门leader 8月份被降级、小组目前业务大部分被拿走 、对目前小组的发展前景不是很看好。 离职好处: 1.工资可以又较大涨幅、寻找有价值业务 2.杭州的话,离黄瀛更近。目前尚不清楚黄瀛是否期望一直在杭州、还是说等关系稳定后可以一起去其他城市打拼。 活水选择:

dainandainan1的博客 1634

Python面试核心考点深度解析:从语言特性到工程实践

Python作为一门应用广泛的高级编程语言,其核心在于对内存管理、对象模型和并发机制的理解。从语言特性层面,可变与不可变对象的行为差异是理解参数传递、哈希与性能优化的基础;装饰器作为高阶函数和闭包的语法糖,是实现横切关注点(如日志、权限)的优雅方案,而生成器则通过惰性求值机制,在处理大规模数据流时展现出卓越的内存效率。在工程实践中,GIL(全局解释器锁)是CPython实现并发时需要考虑的关键因素,它影响了多线程在CPU密集型任务中的并行能力,促使开发者根据场景在**多进程**、多线程与协程之间做出合理选型

weixin_34245749的博客 546

【FDE知识库】(1):AI 编程时代开发语言选型分析,云使用go,企业用java,算法python,底层c

AI 编程时代,开发语言选型需综合考量AI适配度、编译速度、跨平台与学习成本。Python以海量语料和即时执行成首选,适用于AI/ML与原型开发;TypeScript凭借类型系统与秒级构建,成为前端及应用层主流;Go以极速编译与统一风格,契合AI代理高效迭代需求。Rust/C++虽性能强,但编译慢、学习成本高,不适于快速试错。综合推荐:优先选Python、TypeScript、Go,兼顾效率与生态,实现最优AI协作体验。

freewebsys的专栏 523
上一篇: C/C++相关概念和易错知识点(59)(C/C++缓冲区同步刷新、IO流)
下一篇: Linux相关概念和易错知识点(33)(基于阻塞队列和环形队列的生产消费模型实现、sem的应用)
-SGlow-
博客等级 码龄3年 1513粉丝 118原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值