MFC线程安全案例
作者:小蜗牛向前冲
名言:我可以接受失败,但我不能接受放弃
如果觉的博主的文章还不错的话,还请
点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正
目录
一、项目解析
二、多线程安全机制
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_TIMEenum { IDD = IDD_THREADOPERATE_DIALOG };
#endifprotected: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);}});//线程2m_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、项目演示
这里我们点击启动线程,就可以观察到对话框的变化。

相关文章:
MFC线程安全案例
作者:小蜗牛向前冲 名言:我可以接受失败,但我不能接受放弃 如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、项目解析 二…...
【Elasticsearch】bucket_sort
Elasticsearch 的bucket_sort聚合是一种管道聚合,用于对父多桶聚合(如terms、date_histogram、histogram等)的桶进行排序。以下是关于bucket_sort的详细说明: 1.基本功能 bucket_sort聚合可以对父聚合返回的桶进行排序ÿ…...
计算机毕业设计——Springboot点餐平台网站
📘 博主小档案: 花花,一名来自世界500强的资深程序猿,毕业于国内知名985高校。 🔧 技术专长: 花花在深度学习任务中展现出卓越的能力,包括但不限于java、python等技术。近年来,花花更…...
MATLAB中count函数用法
目录 语法 说明 示例 对出现次数计数 使用模式对数字和字母进行计数 多个子字符串的所有出现次数 忽略大小写 对字符向量中的子字符串进行计数 count函数的功能是计算字符串中模式的出现次数。 语法 A count(str,pat) A count(str,pat,IgnoreCase,true) 说明 A c…...
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 轨迹预测新基准!清华开源:统一自动驾驶纵向轨迹数据集 自动驾驶车辆在交通运输领域展现出巨大潜力,而理解其纵向驾驶行为是实现安全高效自动驾驶的关键。现有的开…...
nexus部署及配置https访问
1. 使用docker-compose部署nexus docker-compose-nexus.yml version: "3" services:nexus:container_name: my-nexusimage: sonatype/nexus3:3.67.1hostname: my-nexusnetwork_mode: hostports:- 8081:8081deploy:resources:limits:cpus: 4memory: 8192Mreservations…...
ffmpeg -hwaccels
1. ffmpeg -hwaccels -loglevel quiet 显示ffmpeg支持的硬件设备 2. 输出 Hardware acceleration methods: vdpau cuda vaapi qsv drm opencl 3. 说明 输出中的cuda表示ffmpeg支持Nvidia 硬件设备。编译ffmpeg增加相关硬件设备的配置,输出会显示相应的信…...
Python——批量图片转PDF(GUI版本)
目录 专栏导读1、背景介绍2、库的安装3、核心代码4、完整代码总结专栏导读 🌸 欢迎来到Python办公自动化专栏—Python处理办公问题,解放您的双手 🏳️🌈 博客主页:请点击——> 一晌小贪欢的博客主页求关注 👍 该系列文章专栏:请点击——>Python办公自动化专…...
LabVIEW无人机飞行状态监测系统
近年来,无人机在农业植保、电力巡检、应急救灾等多个领域得到了广泛应用。然而,传统的目视操控方式仍然存在以下三大问题: 飞行姿态的感知主要依赖操作者的经验; 飞行中突发的姿态异常难以及时发现; 飞行数据缺乏系统…...
算法16(力扣451)——根据字符出现频率排序
1、问题 给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的频率 是它出现在字符串中的次数, 返回 已排序的字符串。如果有多个答案,返回其中任何一个。 2、示例 (1) 输入: s "tree&q…...
Response 和 Request 介绍
怀旧网个人博客网站地址:怀旧网,博客详情:Response 和 Request 介绍 1、HttpServletResponse 1、简单分类 2、文件下载 通过Response下载文件数据 放一个文件到resources目录 编写下载文件Servlet文件 public class FileDownServlet exten…...
ADB详细教程
目录 一、ADB简介 二、配置 配置环境变量 验证是否安装成功 三、简单使用 基本命令 设备连接管理 USB连接 WIFI连接(需要USB线) 开启手机USB调试模式 开启USB调试 四、其他 更换ADB默认启动端口 一、ADB简介 ADB(Android Debug…...
Jenkins+gitee 搭建自动化部署
Jenkinsgitee 搭建自动化部署 环境说明: 软件版本备注CentOS8.5.2111JDK1.8.0_211Maven3.8.8git2.27.0Jenkins2.319最好选稳定版本,不然安装插件有点麻烦 一、安装Jenkins程序 1、到官网下载相应的版本war或者直接使用yum安装 Jenkins官网下载 直接…...
今日AI和商界事件(2025-02-11)
今日AI大事件主要包括以下几个方面: 一、行业竞购与合作变动 马斯克组团竞购OpenAI 据《华尔街日报》报道,马斯克率投资者财团出价974亿美元竞购OpenAI,欲使其回归开源公益使命。xAI支持此次竞购,若成功,xAI或与OpenA…...
oracle dbms_sqltune 使用
创建测试表 CREATE TABLE test_table (id NUMBER PRIMARY KEY,event_date DATE,value NUMBER );插入测试数据 DECLAREi NUMBER; BEGINFOR i IN 1..1000000 LOOPINSERT INTO test_table (id, event_date, value)VALUES (i, SYSDATE - MOD(i, 365), DBMS_RANDOM.VALUE(1, 1000)…...
大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡 背景 前端开发接口请求,调试,联调,接入数据,前端必不可少工具,postman是一个非常好…...
AI大语言模型
一、AIGC和生成式AI的概念 1-1、AIGC Al Generated Content:AI生成内容 1-2、生成式AI:generative ai AIGC是生成式 AI 技术在内容创作领域的具体应用成果。 目前有许多知名的生成式 AI: 文本生成领域 OpenAI GPT 系列百度文心一言阿里通…...
数智百问 | 制造企业如何降低产线检测数据的存储和管理成本?
在《“十四五”智能制造发展规划》等政策的推动下,以及新能源汽车、消费电子等品牌商对产品质量和供应商智能化水平要求的提升,半导体、电子制造、动力电池等先进制造行业企业纷纷推进产线智能化升级,并投入大量机器视觉检测设备以实现自动化…...
Pdf手册阅读(1)--数字签名篇
原文阅读摘要 PDF支持的数字签名, 不仅仅是公私钥签名,还可以是指纹、手写、虹膜等生物识别签名。PDF签名的计算方式,可以基于字节范围进行计算,也可以基于Pdf 对象(pdf object)进行计算。 PDF文件可能包…...
Python 识别图片和扫描PDF中的文字
目录 工具与设置 Python 识别图片中的文字 Python 识别图片中的文字及其坐标位置 Python 识别扫描PDF中的文字 注意事项 在处理扫描的PDF和图片时,文字信息往往无法直接编辑、搜索或复制,这给信息提取和分析带来了诸多不便。手动录入信息不仅耗时费…...
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学习笔记,主要目的是为了帮助后期快速复习使用 学习视频:7小快速通关SpringCloud 辅助文档:SpringCloud快速通关 源码地址:cloud-demo 一、简介 官网:https://spring.io/projects/spring-clou…...
【JVM详解五】JVM性能调优
示例: 配置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…...
基于 STM32 平台的音频特征提取与歌曲风格智能识别系统
标题:基于 STM32 平台的音频特征提取与歌曲风格智能识别系统 内容:1.摘要 摘要:本文介绍了一种基于 STM32 平台的音频特征提取与歌曲风格智能识别系统。该系统通过对音频信号进行特征提取和分析,实现了对歌曲风格的自动识别。在特征提取方面,…...
DeepSeek:搅动人工智能产业风云的鲶鱼效应深度解读
我的个人主页 我的专栏:人工智能领域,希望能帮助到大家!!!点赞❤ 收藏❤ 引言 在当今科技飞速发展的时代,人工智能(AI)无疑是最为耀眼的领域之一。众多技术与平台如繁星般涌现&a…...
一觉醒来全球编码能力下降100000倍,新手小白的我决定科普C语言——三子棋游戏实现
硬控我一上午,小编还是太菜了,大家可以自行升级电脑难度,也可以升级游戏到五子棋 1.game.h #pragma once #include<stdio.h> #include<stdlib.h> #include<time.h> #define ROW 3 #define COL 3//初始化棋盘 void InitBoa…...
学习 URL 传参中哪些字符是支持的,哪些是不支持的
URL 的结构 URL 由多个部分组成,包括协议、域名、路径、查询参数和片段标识符,其中,查询参数部分就是问号后面的内容,这部分使用键值对,中间用 & 分隔。比如,http://example.com/path?key1value1&…...
bingAI生成的易语言编程基础
易语言编程基础 易语言(EPL)是一种基于中文的编程语言,旨在简化编程学习过程,特别适合初学者和有一定编程基础的开发者。它通过中文关键词和语法,降低了编程的门槛,使得代码更加直观易懂。 示例ÿ…...
HTML应用指南:利用POST请求获取接入比亚迪业态的充电桩位置信息
在新能源汽车快速发展的今天,充电桩的分布和可用性成为了影响用户体验的关键因素之一。比亚迪作为全球领先的新能源汽车制造商,不仅在车辆制造方面取得了卓越成就,也在充电基础设施建设上投入了大量资源。为了帮助用户更方便地找到比亚迪充电桩的位置,本篇文章,我们将探究…...
