【多线程学习】线程池、简单的银行取款模拟

2023-09-21 10:45:41

学习代码如下,教程来自:http://www.seestudy.cn/?list_9/42.html

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <functional>
// 1
void printFun(std::string msg) {
	std::cout << msg << std::endl;
	//for (int i = 0; i < 1000; i++) {
	//	std::cout << i;
	//}
	return;
}

void printHelloWorld() {
	std::cout << "Hello World" << std::endl;
	return;
}

// 2
void foo(int& x) {
	x += 1;
}

// 3、线程安全和互斥锁
int a = 0;
std::mutex mtx;
void func() {
	for (int i = 0; i < 100000; i++) {
		mtx.lock();
		a += 1;
		mtx.unlock();
	}
}

// 4、互斥死锁
int c = 0;
std::mutex mtx1;
std::mutex mtx2;
void func1() {
	for (int i = 0; i < 400; i++) {
		mtx1.lock();
		mtx2.lock();
		c += 1;
		mtx1.unlock();
		mtx2.unlock();
	}
}

void func2() {
	for (int i = 0; i < 400; i++) {
		mtx1.lock();
		mtx2.lock();
		c += 1;
		mtx1.unlock();
		mtx2.unlock();
	}
}

// 5
std::timed_mutex mtx3;
int d = 0;
void func3() {
	for (int i = 0; i < 2; i++) {
		//std::lock_guard<std::mutex> lg(mtx3);
		std::unique_lock<std::timed_mutex> lg(mtx3, std::defer_lock);
		if (lg.try_lock_for(std::chrono::seconds(2))) {
			std::this_thread::sleep_for(std::chrono::seconds(1));
			d += 1;
		}
		
	}
}

// 6 单例模式
class Log {
private:
	Log() {};//单例模式要保证构造函数私有,然后在类里面定义静态函数得到一个属于整个类的实例
	//static Log* log;
	//static std::once_flag initFlag;  // once_flag 对象
public:

	Log(const Log& log) = delete;
	Log& operator=(const Log& log) = delete;

	static Log& GetInstance() {
		//饿汉模式,线程安全
		static Log log;
		return log;

		//懒汉模式,多线程必须加锁
		//if (!log) log = new Log;
		//std::call_once(initFlag, []() {  // 使用 lambda 函数创建单例实例
		//	log = new Log;
		//	});
		//return *log;
	}

	void printLog(std::string msg) {
		std::cout << __TIME__ << " " << msg << std::endl;
	}
};
//Log* Log::log = nullptr;
//std::once_flag Log::initFlag;

void func4() {
	Log::GetInstance().printLog("error");
}

// 7、生产者消费者模型
std::queue<int> g_queue;
std::condition_variable g_cv;
std::mutex mtx_produce;
void Producer() { //生产者
	for (int i = 0; i < 10; i++) {
		std::unique_lock<std::mutex> lock(mtx_produce);
		g_queue.push(i);
		//通知消费者取任务
		g_cv.notify_one();
		std::cout << "Producer : " << i << std::endl;

		std::this_thread::sleep_for(std::chrono::microseconds(500));
	}
	
}

void Consumer() { //消费者
	while (1) {
		std::unique_lock<std::mutex> lock(mtx_produce);
		g_cv.wait(lock, []() {return !g_queue.empty(); });
		int value = g_queue.front();
		g_queue.pop();
		std::cout << "Consumer : " << value << std::endl;
	}
}

// 8、线程池
class ThreadPool{
public:
	ThreadPool(int numThreads) :stop(false) { //构造函数
		for (int i = 0; i < numThreads; i++) {
			threads.emplace_back([this] { //每个线程执行的操作
				while (1) {
					std::unique_lock<std::mutex> lock(mtxPool); //1、加锁
					condition.wait(lock, [this] { //2、将线程置于休眠状态,直到有任务可用或线程池停止
						return !tasks.empty() || stop; //如果有新任务了或者线程池要关闭了,则退出休眠状态
						});

					if (stop && tasks.empty()) { //如果线程池关闭且任务队列为空,直接返回
						return;
					}

					std::function<void()> task(std::move(tasks.front())); //3、从任务队列中取任务,std::move减少拷贝,移动语义直接转移资源
					tasks.pop(); 
					lock.unlock(); //4、解锁
					task();
				}
			} //每个线程操作结束
			);
		}
	}

	~ThreadPool() { //析构函数
		{
			std::unique_lock<std::mutex> lock(mtxPool);
			stop = true; 
		}
		
		condition.notify_all(); //通知所有线程要关闭了,但线程不会立即停止,它们会在检查stop变量后做出决策
		for (auto& t : threads) {
			t.join(); //等待所有线程执行完
		}
	}

	template<class F, class... Args>
	void enqueue(F&& f, Args&&... args) { //将任务封装成std::function 对象,并添加到任务队列中
		std::function<void()> task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
		{
			std::unique_lock<std::mutex> lock(mtxPool);
			tasks.emplace(std::move(task)); 
		}

		condition.notify_one(); //通知一个休眠的线程有新的任务可以取
	};
	

private:
	std::vector<std::thread> threads; //线程数组
	std::queue<std::function<void()>> tasks; //任务队列
	std::mutex mtxPool; //互斥量
	std::condition_variable condition; //条件变量
	bool stop; //线程池停止的标记
};


int main() {
	
	//1、创建线程
	std::thread thread1(printFun, "Hello Thread");
	bool isJoin = thread1.joinable();
	if (isJoin) {
		thread1.join();
	}
	std::cout << "over" << std::endl;
	std::thread thread2(printHelloWorld);
	thread2.join();

	int b = 1;
	std::thread thread3(foo, std::ref(b));
	thread3.join();

	//3、线程冲突
	std::thread t1(func);
	std::thread t2(func);
	t1.join();
	t2.join();
	std::cout << a << std::endl;

	//4、互斥量死锁

	std::thread t3(func1);
	std::thread t4(func2);
	t3.join();
	t4.join();
	std::cout << c << std::endl;

	//5、lock_guard 与 std::unique_lock
	std::thread t5(func3);
	std::thread t6(func3);
	t5.join();
	t6.join();
	std::cout << d << std::endl;

	//6、单例模式
	//Log a1; 该行会报错,因为单例模式只能有一个实例,就是在类里面
	std::thread t7(func4);
	std::thread t8(func4);
	t7.join();	
	t8.join();

	//7、生产者与消费者模型
	std::thread t9(Producer);
	std::thread t10(Consumer);
	t9.join();
	t10.join();
	

	//8、跨平台线程池
	ThreadPool pool(4);
	for (int i = 0; i < 10; i++) {
		pool.enqueue([i] { //加任务
			std::cout << "task :" << i << " is running " << std::endl;
			std::this_thread::sleep_for(std::chrono::seconds(1));
			std::cout << "task :" << i << " is done " << std::endl;
			});
	}
	return 0;
}

线程池解读:

在示例中,我们定义了一个 ThreadPool
类,并且在构造函数中创建了指定数目的线程。在每个线程中,我们不断地从任务队列中获取任务并执行,直到线程池被停止。在 enqueue()
函数中,我们将任务封装成一个 std::function 对象,并将它添加到任务队列中。在 ThreadPool
的析构函数中,我们等待所有线程执行完成后再停止所有线程。

在主函数中,我们创建了一个 ThreadPool 对象,并向任务队列中添加了 8 个任务。每个任务会输出一些信息,并且在执行完后等待 1
秒钟。由于线程池中有 4 个线程,因此这 8 个任务会被分配到不同的线程中执行。在任务执行完成后,程序会退出。

对于多线程enqueue函数的解读:
该函数作用是将用户传入的函数(以及其参数)封装为一个可以无参数调用的任务,并将这个任务添加到任务队列中。下面逐行解释代码:

template<class F, class... Args>:模板参数声明。F用于匹配用户传入的函数类型,Args…是一个可变模板参数,用于匹配用户传入的函数参数。

void enqueue(F&& f, Args&&... args):enqueue函数的定义,其中使用了右值引用(&&)来接受传入的函数和其参数。这样可以有效地处理传入的临时对象和实现完美转发。

std::function<void()> task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);:使用std::bind将用户传入的函数和其参数绑定起来,创建一个可以无参数调用的函数对象。
std::forward<F>(f)和std::forward<Args>(args)...是完美转发的技巧,它确保传入enqueue的参数以其原始形式(lvalue或rvalue)传递给std::bind。
将绑定后的函数对象赋给std::function<void()>类型的task,这样可以统一地处理任务,不管用户传入的函数有什么签名。
{ 一个局部代码块的开始。确保std::unique_lock的作用范围仅限于这个块,这样当块结束时,锁会自动释放。
std::unique_lock<std::mutex> lock(mtxPool);:使用std::unique_lock锁住mtxPool互斥量,确保在修改任务队列时不会有其他线程同时修改。

tasks.emplace(std::move(task));:将task移动到任务队列中。使用std::move是为了避免不必要的拷贝,而是将task的资源转移到队列中的新任务对象。

} 局部代码块结束,这时std::unique_lock会自动释放锁。

condition.notify_one();:使用条件变量condition通知一个正在等待的线程:任务队列中有了新的任务,可以进行处理了。

通过这个函数,用户可以很方便地将任意函数(及其参数)提交给线程池执行。

自己写的一个模拟银行窗口取钱的小程序,目的是为了更好地理解和应用多线程。
比较简单的一版,用了std::mutex mtx; mtx.lock()手动上锁和mtx.unlock()解锁:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int sumMoney = 10000;
mutex mtx;

void drawMoney(string window, int num) {
	while (sumMoney > 0) {
		mtx.lock();
		if (sumMoney >= num) {
			sumMoney -= num;
			cout << window << "取 " << num << " 元钱,现金储备还剩:" << sumMoney << endl;
		}
		mtx.unlock();
		this_thread::sleep_for(chrono::microseconds(100)); //保证每个窗口都有概率取到钱
	}
}

int main() {
	thread t1(drawMoney, "银行窗口1", 100);
	thread t2(drawMoney, "银行窗口2", 200);
	thread t3(drawMoney, "银行窗口3", 500);
	t1.join();
	t2.join();
	t3.join();
}

第二版想试试condition_variable,但实际上没有必要用,因为条件变量主要用于复杂的同步场景,例如当某个线程需要等待特定条件满足时(例如任务队列中有任务可供处理),本问题
这个简单场景中核心在于线程间的取款操作同步,而不是等待特定的条件发生。因此,使用std::mutex就足够了。通过适当地加锁、解锁以及合理地使用线程调度提示(如this_thread::yield()),可以使多个线程有均匀的取款机会。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

int sumMoney = 10000;
mutex mtx;
condition_variable cv;

void drawMoney(string window, int num) {
    unique_lock<mutex> lock(mtx);

    while (sumMoney > 0) {
        while (sumMoney < num) {  // 如果不够这个线程取,就等待
            cv.wait(lock);
        }

        sumMoney -= num;
        cout << window << "取 " << num << " 元钱,现金储备还剩:" << sumMoney << endl;
        cv.notify_all();  // 通知所有线程,因为余额发生了变化
    }
}

int main() {
    thread t1(drawMoney, "银行窗口1", 100);
    thread t2(drawMoney, "银行窗口2", 200);
    thread t3(drawMoney, "银行窗口3", 500);
    t1.join();
    t2.join();
    t3.join();
}
更多推荐

【全志V3s】SPI NAND Flash 驱动开发

文章目录一、硬件介绍V3s的启动顺序二、驱动支持U-Boot驱动主线Linux驱动已经支持三、烧录工具xfel四、构建U-Boot(官方的Uboot)先编译一下开始spinandflash代码层面的适配修改menuconfig配置ARMarchitecture配置SupportforSPINandFlashonAllw

MySQL远程登录提示Access denied的场景

厂商给的某个MySQL库,通过客户端远程登录,提示这个错误,Accessdeniedforuser'用户名'@'IP'(usingpassword:YES)确认输入的账号密码都是正确的,出现这个错误说明端口是通的。此时可以检索mysql.user,如果待登录账号的记录host字段是localhost,说明仅允许本地登录

Hbuilder本地调试微信H5项目(一)

摘要通过内网穿透,访问本地Hbuilder创建的Vue项目前置准备下载并安装【HBuilder】,本文用的是HBuilder3.8.12版本,下载地址下载并安装【微信开发者工具】,本文用的是1.06版本,下载地址下载并安装【natapp】,下载地址实现逻辑本地使用Hbuilder进行开发并运行起来(配置为80端口)使用

【golang】深入理解GMP调度模型

GoroutineGo中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完,这就能在有限的内存空间内支持大量goroutine,支持了更多的并发,虽然一个goroutine的栈只占几KB(Go语言官方说明为4~5KB),但实际是可伸缩的,如果需要更多

性能测试 —— Jmeter定时器

固定定时器如果你需要让每个线程在请求之前按相同的指定时间停顿,那么可以使用这个定时器;需要注意的是,固定定时器的延时不会计入单个sampler的响应时间,但会计入事务控制器的时间1、使用固定定时器位置在http请求中;每次http请求前延迟3秒;配置路径——定时器——固定定时器;如下图:2、线程组循环3次,通过表格查看

启山智软/电商商城100%开源

介绍想要了解代码规范,学习商城解决方案,点击下方官网链接联系客服作者:启山智软官网及博客:启山智软官网、CSDN、掘金、gitee简介:启山智软目前开发了全渠道电商商城系统,本商城是基于SpringCloud的商城系统,百万真实用户沉淀并检验的商城。注意:该项目只提供学习,切勿用于商业用途电商商城是什么:电商商城指的是

json数据解析

目录一、读数据1、简单对象读取2、数组读取3、对象读取二、写数据1、简单生成JSON2、对象数组JSON3、嵌套对象三、一个综合例子1、读JSON2、写JSON一、读数据1、简单对象读取{"app":"xnwVideo","src":"C:\\build-video\\Output","dest":"C:\\build

thinkphp:查询本周中每天中日期的数据,查询今年中每个月的数据,查询近五年每年的总数据

一、查询本周中每天中日期的数据结果:以今天2023-09-14为例,这一周为2023-09-11~2023-09-07代码后端thinkphp://查询本周每天的的总金额数//获取本周的起始日期和结束日期$weekStart=date('Y-m-d',strtotime('thisweekMonday'));$week

零基础Linux_5(开发工具_上)yum和vim和gcc/g++和gdb

目录1.软件包管理器yum1.1安装软件的方式1.2yum指令2.vim(编辑器)2.1vim的简单操作2.1.1方向键(HJKL)2.1.2退出vim2.2vim文本批量化操作(命令模式)2.2.1复制.粘贴.删除.剪贴.撤销2.2.2光标跳转2.2.3vim其它操作2.3配置vim3.gcc和g++3.1程序的翻译

leetcode363周赛

2859.计算K置位下标对应元素的和核心思想:枚举+调库,比较简单这题。2860.让所有学生保持开心的分组方法数核心思想:枚举选择学生的人数,首先选0个,选1个,选2个,选3个...;由于要满足题目要求得到一个结论我们需要优先选择nums[i]小的(具体证明可以看b站灵神视频),当时我有一个疑问比如选择三个学生,这三个

CRM软件系统对外贸行业的解决方案

国内的外贸行业经历了四个发展阶段,从发展期到繁荣期,CRM客户管理系统逐步走到幕前,成为外贸企业必不可少的主打工具。那么外贸行业正面临哪些问题?该如何解决?下面我们就来说说适合外贸行业的CRM解决方案。外贸行业的压力和困境外贸行业向来都是机遇与挑战并存。每年都有商业领袖行业大咖高呼外贸的春天要来了,可外贸人自己感受到的

热文推荐