MFC线程安全案例

news2025/2/12 16:59:19

作者:小蜗牛向前冲

名言:我可以接受失败,但我不能接受放弃

  如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正

目录

一、项目解析

二、多线程安全机制

2.1、互斥锁

2.2、临界区

三、项目实现

3.1、在Dialg资源UI控制好界界面

3.2、通过类封装 线程操作

3.3、通过消息映射机制处理点击事件

3.4、项目演示 


一、项目解析

这里我们就要控制线程对临界资源的访问,通过UI界面进行测试。 

二、多线程安全机制

在多线程中,当多个线程访问同一份临界资源的时候,可能会出现线程之间竞争的操作,比如有二个线程A和B,他们都要进行抢票操作, int ticket=5000,线程每次抢到一份票就进行ticket--操作。

每个线程中都有下面的判断

while (1)
{
	if (ticket <= 0)
	{
		std::cout << "停止抢票"<<std::endl;
			break;
	}
	else
	{
		--ticket;
	}
}

当没有票的时候,就停止抢票。但是我们发现ticket会出现负数的情况。按理说是这是不可能的,为什么呢?这是因为--操作是非原子的。

原子操作(Atomic Operation)

原子操作是指一组操作在执行过程中不可被中断的操作。无论在多线程或并发环境中,原子操作都是一个不可分割的单位,即使在操作执行过程中,其他线程也无法干扰该操作。这保证了操作的完整性和一致性。

非原子操作(Non-Atomic Operation)

非原子操作是指在执行过程中可能会被其他线程或进程中断的操作。在多线程或并发环境下,非原子操作可能会导致数据竞争、冲突或不一致的结果。

那为什么会出现负数呢?

 首先,我们来分析一下非原子操作:

--ticket;  // 非原子操作

这个操作实际上包含了三个步骤: 

  • 读取 ticket 的当前值。
  • 对该值进行减 1 操作。
  • 将新的值写回 ticket

在多线程环境中,当多个线程(比如线程A和线程B)同时执行这段代码时,可能会出现以下情况:

场景示例:

假设 ticket 的值为 1,线程 A 和线程 B 同时开始抢票。

  • 线程A 读取了 ticket 的值为1,刚刚执行完--操作,但是还没有来的急,将信息写会回内存变量,OS(操作系统就调度线程B进行抢票)
  • 线程B此时发现ticket 的值为1,可以进行抢票,线程B也执行了--操作。
  • 这个时候线程A的时间片到了,继续执行后面的操作把结果写会变量为0
  • 这个线程B继续往后执行,也讲--的结果写会,最终ticketw为服数

这样,ticket 同一份资源被用了二个线程同时使用,造成了资源不一致,当多个线程都在进行类似的操作时,ticket 的值有可能被多次修改,最终可能导致 ticket 为负数。

为了解决在多线程中,那些线程不安全的问题,我们要使用下面的安全操作

2.1、互斥锁

互斥锁(Mutex,全称 Mutual Exclusion Lock)是一种用于 多线程同步 的机制,主要用于保护共享资源,确保同一时刻只有一个线程能够访问共享数据,从而避免数据竞争和不一致性。互斥锁通过加锁和解锁操作来控制对临界区的访问。

C++11 引入了 std::mutex 类来提供互斥锁的功能。它位于 <mutex> 头文件中。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // 创建一个互斥锁

void printHello() {
    mtx.lock();  // 加锁
    std::cout << "Hello from thread!" << std::endl;
    mtx.unlock();  // 解锁
}

int main() {
    std::thread t1(printHello);
    std::thread t2(printHello);

    t1.join();
    t2.join();

    return 0;
}

 使用 std::lock_guard 自动管理锁

std::lock_guard 是一个 RAII(Resource Acquisition Is Initialization)类,它用于自动加锁和解锁。std::lock_guard 在创建时会加锁,在销毁时会自动解锁。这种方式更安全,避免了手动解锁时可能出现的错误(例如忘记解锁

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void printHello() {
    std::lock_guard<std::mutex> lock(mtx);  // 自动加锁
    std::cout << "Hello from thread!" << std::endl;
}  // 自动解锁

int main() {
    std::thread t1(printHello);
    std::thread t2(printHello);

    t1.join();
    t2.join();

    return 0;
}

std::unique_lock:更灵活的锁管理 

std::unique_lock 是另一种锁管理类,它提供了更多的功能,如可以手动解锁、延迟加锁、以及可以在多个锁上进行组合。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void printHello() {
    std::unique_lock<std::mutex> lock(mtx);  // 加锁
    std::cout << "Hello from thread!" << std::endl;
    // lock 会在作用域结束时自动解锁
}

int main() {
    std::thread t1(printHello);
    std::thread t2(printHello);

    t1.join();
    t2.join();

    return 0;
}

锁的死锁(Deadlock) 

死锁是指两个或多个线程因相互等待而无法继续执行的情况。例如,线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1。这样,两个线程相互等待对方释放锁,导致程序进入死锁状态。

2.2、临界区

临界区是指一段在多线程程序中访问共享资源的代码区域,多个线程必须互斥地访问这个区域,以避免出现数据竞争和资源冲突。简单来说,临界区是多个线程共享的数据被操作的区域,这些数据需要保护起来,以确保在任何时刻只有一个线程能够访问和修改这些共享数据。

Windows 操作系统中,创建和使用临界区的 API 提供了多线程同步的机制。Windows 提供了 CRITICAL_SECTION 类型,它是操作系统用于线程同步的基本工具。CRITICAL_SECTION 用于保护共享资源,确保同一时刻只有一个线程能够进入临界区。

1. 初始化临界区

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

lpCriticalSection:指向一个 CRITICAL_SECTION 结构的指针,系统会根据这个结构初始化临界区。 

2. 删除临界区

当不再需要临界区时,应该调用 DeleteCriticalSection 来销毁它。这样可以释放与临界区相关的资源。

void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

3. 加锁临界区 

使用 EnterCriticalSection 函数来加锁临界区。当一个线程试图访问临界区时,它必须首先请求锁。如果锁已经被其他线程持有,则该线程将会被阻塞,直到锁变得可用。 

void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

 4. 解锁临界区

使用 LeaveCriticalSection 来解锁临界区,这样其他线程可以访问临界区中的资源。

void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

要使用临界资源,我们要先定义应该临界区,比如: CRITICAL_SECTION m_critical_section对象

,然后调用相应的函数就好了。如EnterCriticalSection(&m_CriticalSection);

三、项目实现

为了实现这个项目,我们通过MFC创建应该基于对话框的项目。

3.1、在Dialg资源UI控制好界界面

这里要注意为对话框添加变量名,因为我要显示线程运行的数据在上面。

这里我们在对话框的.h头文件中可以看到 

 

3.2、通过类封装 线程操作

 ThreadMagager.h在这个头文件中实现

#pragma once

#include<functional>
#include<thread>
#include<atomic>
#include <windows.h> 
#include <memory> 
namespace pjb
{
	class ThreadManager
	{
	public:
		ThreadManager();
		~ThreadManager();

		void StartThread(int step, unsigned int interval, std::function<void(int)> callback);
		void StopThread();
		bool IsRunning()const { return m_Running; };

	private:
		std::thread m_Thread;
		//std::unique_ptr<std::thread> m_Thread;
		std::atomic<bool> m_Running;//创建原子对象,保证线程安全
		int m_iCount;//计数器
		int m_iStep;//步长
		unsigned int m_uiInterval;//时间间隔
		std::function<void(int)> m_UpdateCallback;//更新回调函数
		CRITICAL_SECTION m_CriticalSection; // 临界区,用于线程同步

		void _ThreadProc();
	};
}

在 ThreadMagager.cpp中实现细节操作


#include "pch.h"//这个头我文件记得包含
#include"ThreadMagager.h"
#include<chrono>


// 构造函数:初始化成员变量和临界区
pjb::ThreadManager::ThreadManager()
	: m_Running(false), m_iCount(0), m_iStep(0), m_uiInterval(0)
{
	InitializeCriticalSection(&m_CriticalSection); // 初始化临界区
}

// 析构函数:停止线程并销毁临界区
pjb::ThreadManager::~ThreadManager()
{
	StopThread();
	DeleteCriticalSection(&m_CriticalSection); // 销毁临界区
}
void pjb::ThreadManager::StartThread(int step, unsigned int interval, std::function<void(int)> callback)
{
	//判断线程是否已经启动
	if (m_Running) return;

	//设置线程的参数
	m_iStep = step;
	m_uiInterval = interval;
	m_UpdateCallback = callback;
	m_Running = true;

	//启动线程
	 // 使用智能指针来管理资源,防止内存泄漏
	//m_Thread = std::make_unique<std::thread>(&ThreadManager::ThreadProc, this);
	//std::thread t(function, args...);的参数
	//function为可调用对象,将会做为线程的主体函数。传this指针,是为了告诉线程在哪个 ThreadManager 对象上调用这个成员函数
	m_Thread = std::thread(&pjb::ThreadManager::_ThreadProc, this);

}
void pjb::ThreadManager::StopThread()
{
	if (!m_Running)return;

	m_Running = false;

	//判断线程是否能被join()。
	if (m_Thread.joinable())
   {
	//m_Thread.join();//等待线程完成,这种方式会阻塞UI线程,导致用户无法操作
	  m_Thread.detach();  // 分离线程,让它在后台执行,UI线程可以继续操作,当程序结束后,操作系统会回收资源
    }
}
//线程处理
void pjb::ThreadManager::_ThreadProc()
{
	while (m_Running)
	{
		// 使用临界区保护共享资源
		EnterCriticalSection(&m_CriticalSection);

		// 检查计数器是否已经达到上限
		if (m_iCount >= 10000)
		{
			m_Running = false; // 停止线程
			LeaveCriticalSection(&m_CriticalSection);  // 离开临界区
			break; // 退出循环
		}
		//更新计数器
		m_iCount += m_iStep;

		LeaveCriticalSection(&m_CriticalSection); // 离开临界区
		//调用回调函数,更新计数值
		if (m_UpdateCallback)
		{
			m_UpdateCallback(m_iCount);
		}

		//休眠指定时间,然后继续进行更新
		std::this_thread::sleep_for(std::chrono::milliseconds(m_uiInterval));//毫秒

	}
}


这里我们在 线程处理函数_ThreadProc()中创建临界区,限制了访问临界资源时候只运行一个线程访问。

3.3、通过消息映射机制处理点击事件

对于对话框的头文件没有什么好说的,大家看一下就可以了。

// ThreadOperateDlg.h: 头文件
//

#pragma once
#include"ThreadMagager.h"

// CThreadOperateDlg 对话框
class CThreadOperateDlg : public CDialogEx
{
// 构造
public:
	CThreadOperateDlg(CWnd* pParent = nullptr);	// 标准构造函数

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_THREADOPERATE_DIALOG };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV 支持


// 实现
protected:
	HICON m_hIcon;

	// 生成的消息映射函数
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	DECLARE_MESSAGE_MAP()
private:
	pjb::ThreadManager m_Thread1;
	pjb::ThreadManager m_Thread2;
	CEdit m_show1;
	CEdit m_show2;
public:
	afx_msg void OnBnClickedButton1();
	afx_msg void OnBnClickedButton2();

在对话框.cpp文件中处理点击事情,就是通过前面我们封装好的接口去处理即可

//线程启动
void CThreadOperateDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	//启动线程
	//情况2使用共享计数变量:
	m_Thread1.StartThread(1, 10, [this](int value) {
		CString str;
		//格式转换,将整形转换为字符串
		str.Format(_T("%d"), value);
		GetDlgItem(IDC_EDIT1)->SetWindowTextW(str);
		if (_ttoi(str) >= 10000)
		{
			//将启动线程的按键禁用
			GetDlgItem(IDC_BUTTON1)->EnableWindow(FALSE);
		}
		});

	//线程2
	m_Thread2.StartThread(2, 20, [this](int value) {
		CString str;
		//格式转换,将整形转换为字符串
		str.Format(_T("%d"), value);
		GetDlgItem(IDC_EDIT2)->SetWindowTextW(str);
		if (_ttoi(str) >= 10000)
		{
			//将启动线程的按键禁用
			GetDlgItem(IDC_BUTTON1)->EnableWindow(FALSE);
		}
		});

}


void CThreadOperateDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	m_Thread1.StopThread();
	m_Thread2.StopThread();
}

3.4、项目演示 

这里我们点击启动线程,就可以观察到对话框的变化。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2296940.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Win11下搭建Kafka环境

目录 一、环境准备 二、安装JDK 1、下载JDK 2、配置环境变量 3、验证 三、安装zookeeper 1、下载Zookeeper安装包 2、配置环境变量 3、修改配置文件zoo.cfg 4、启动Zookeeper服务 4.1 启动Zookeeper客户端验证 4.2 启动客户端 四、安装Kafka 1、下载Kafka安装包…

51c自动驾驶~合集49

我自己的原文哦~ https://blog.51cto.com/whaosoft/13164876 #Ultra-AV 轨迹预测新基准&#xff01;清华开源&#xff1a;统一自动驾驶纵向轨迹数据集 自动驾驶车辆在交通运输领域展现出巨大潜力&#xff0c;而理解其纵向驾驶行为是实现安全高效自动驾驶的关键。现有的开…

Python——批量图片转PDF(GUI版本)

目录 专栏导读1、背景介绍2、库的安装3、核心代码4、完整代码总结专栏导读 🌸 欢迎来到Python办公自动化专栏—Python处理办公问题,解放您的双手 🏳️‍🌈 博客主页:请点击——> 一晌小贪欢的博客主页求关注 👍 该系列文章专栏:请点击——>Python办公自动化专…

LabVIEW无人机飞行状态监测系统

近年来&#xff0c;无人机在农业植保、电力巡检、应急救灾等多个领域得到了广泛应用。然而&#xff0c;传统的目视操控方式仍然存在以下三大问题&#xff1a; 飞行姿态的感知主要依赖操作者的经验&#xff1b; 飞行中突发的姿态异常难以及时发现&#xff1b; 飞行数据缺乏系统…

算法16(力扣451)——根据字符出现频率排序

1、问题 给定一个字符串 s &#xff0c;根据字符出现的 频率 对其进行 降序排序 。一个字符出现的频率 是它出现在字符串中的次数&#xff0c; 返回 已排序的字符串。如果有多个答案&#xff0c;返回其中任何一个。 2、示例 &#xff08;1&#xff09; 输入: s "tree&q…

Response 和 Request 介绍

怀旧网个人博客网站地址&#xff1a;怀旧网&#xff0c;博客详情&#xff1a;Response 和 Request 介绍 1、HttpServletResponse 1、简单分类 2、文件下载 通过Response下载文件数据 放一个文件到resources目录 编写下载文件Servlet文件 public class FileDownServlet exten…

ADB详细教程

目录 一、ADB简介 二、配置 配置环境变量 验证是否安装成功 三、简单使用 基本命令 设备连接管理 USB连接 WIFI连接&#xff08;需要USB线&#xff09; 开启手机USB调试模式 开启USB调试 四、其他 更换ADB默认启动端口 一、ADB简介 ADB&#xff08;Android Debug…

Jenkins+gitee 搭建自动化部署

Jenkinsgitee 搭建自动化部署 环境说明&#xff1a; 软件版本备注CentOS8.5.2111JDK1.8.0_211Maven3.8.8git2.27.0Jenkins2.319最好选稳定版本&#xff0c;不然安装插件有点麻烦 一、安装Jenkins程序 1、到官网下载相应的版本war或者直接使用yum安装 Jenkins官网下载 直接…

大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡

大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡 背景 前端开发接口请求&#xff0c;调试&#xff0c;联调&#xff0c;接入数据&#xff0c;前端必不可少工具&#xff0c;postman是一个非常好…

AI大语言模型

一、AIGC和生成式AI的概念 1-1、AIGC Al Generated Content&#xff1a;AI生成内容 1-2、生成式AI&#xff1a;generative ai AIGC是生成式 AI 技术在内容创作领域的具体应用成果。 目前有许多知名的生成式 AI&#xff1a; 文本生成领域 OpenAI GPT 系列百度文心一言阿里通…

Pdf手册阅读(1)--数字签名篇

原文阅读摘要 PDF支持的数字签名&#xff0c; 不仅仅是公私钥签名&#xff0c;还可以是指纹、手写、虹膜等生物识别签名。PDF签名的计算方式&#xff0c;可以基于字节范围进行计算&#xff0c;也可以基于Pdf 对象&#xff08;pdf object&#xff09;进行计算。 PDF文件可能包…

Python 识别图片和扫描PDF中的文字

目录 工具与设置 Python 识别图片中的文字 Python 识别图片中的文字及其坐标位置 Python 识别扫描PDF中的文字 注意事项 在处理扫描的PDF和图片时&#xff0c;文字信息往往无法直接编辑、搜索或复制&#xff0c;这给信息提取和分析带来了诸多不便。手动录入信息不仅耗时费…

C++引用深度详解

C引用深度详解 前言1. 引用的本质与核心特性1.1 引用概念1.2 核心特性 2. 常引用与权限控制2.1 权限传递规则2.2 常量引用2.3 临时变量保护1. 样例2. 样例3. 测试 三、引用使用场景分析3.1 函数参数传递输出型参数避免多级指针高效传参 3.2 做函数返回值正确使用危险案例 4. 性…

SpringCloud - Gateway 网关

前言 该博客为Sentinel学习笔记&#xff0c;主要目的是为了帮助后期快速复习使用 学习视频&#xff1a;7小快速通关SpringCloud 辅助文档&#xff1a;SpringCloud快速通关 源码地址&#xff1a;cloud-demo 一、简介 官网&#xff1a;https://spring.io/projects/spring-clou…

【JVM详解五】JVM性能调优

示例&#xff1a; 配置JVM参数运行 #前台运行 java -XX:MetaspaceSize-128m -XX:MaxMetaspaceSize-128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio8 - XX:UseConcMarkSweepGC -jar /jar包路径 #后台运行 nohup java -XX:MetaspaceSize-128m -XX:MaxMetaspaceS…

DeepSeek:搅动人工智能产业风云的鲶鱼效应深度解读

我的个人主页 我的专栏&#xff1a;人工智能领域&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 引言 在当今科技飞速发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;无疑是最为耀眼的领域之一。众多技术与平台如繁星般涌现&a…

bingAI生成的易语言编程基础

易语言编程基础 易语言&#xff08;EPL&#xff09;是一种基于中文的编程语言&#xff0c;旨在简化编程学习过程&#xff0c;特别适合初学者和有一定编程基础的开发者。它通过中文关键词和语法&#xff0c;降低了编程的门槛&#xff0c;使得代码更加直观易懂。 示例&#xff…

DeepSeek AI R1推理大模型API集成文档

DeepSeek AI R1推理大模型API集成文档 引言 随着自然语言处理技术的飞速发展&#xff0c;大语言模型在各行各业的应用日益广泛。DeepSeek R1作为一款高性能、开源的大语言模型&#xff0c;凭借其强大的文本生成能力、高效的推理性能和灵活的接口设计&#xff0c;吸引了大量开发…

【算法-动态规划】、魔法卷轴: 两次清零机会整个数组最大累加和

【算法-动态规划】、魔法卷轴: 两次清零机会整个数组最大累加和 文章目录 一、dp1.1 题意理解1.2 整体思路1.3 具体思路1.4 代码 二、多语言解法 一、dp 1.1 题意理解 nums 数组, 有正负0, 使用最多两次魔法卷轴, 希望使数组整体的累加和尽可能大. 求尽可能大的累加和 其实就…

【R】Dijkstra算法求最短路径

使用R语言实现Dijkstra算法求最短路径 求点2、3、4、5、6、7到点1的最短距离和路径 1.设置data&#xff0c;存放有向图信息 data中每个点所在的行序号为起始点序号&#xff0c;列为终点序号。 比如&#xff1a;值4的坐标为(1,2)即点1到点2距离为4&#xff1b;值8的坐标为(6,7)…