MFC线程安全案例

作者:小蜗牛向前冲

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

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

目录

一、项目解析

二、多线程安全机制

2.1、互斥锁

2.2、临界区

三、项目实现

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

[3.2、通过类封装 线程操作](#3.2、通过类封装 线程操作)

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

3.4、项目演示


一、项目解析

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

二、多线程安全机制

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

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

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

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

原子操作(Atomic Operation)

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

非原子操作(Non-Atomic Operation)

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

那为什么会出现负数呢?

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

cpp 复制代码
--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> 头文件中。

cpp 复制代码
#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 在创建时会加锁,在销毁时会自动解锁。这种方式更安全,避免了手动解锁时可能出现的错误(例如忘记解锁

cpp 复制代码
#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 是另一种锁管理类,它提供了更多的功能,如可以手动解锁、延迟加锁、以及可以在多个锁上进行组合。

cpp 复制代码
#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. 初始化临界区
cpp 复制代码
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

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

  1. 删除临界区

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

cpp 复制代码
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
  1. 加锁临界区

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

cpp 复制代码
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
  1. 解锁临界区

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

cpp 复制代码
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

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

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

三、项目实现

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

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

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

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

3.2、通过类封装 线程操作

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

cpp 复制代码
#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中实现细节操作

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、通过消息映射机制处理点击事件

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

cpp 复制代码
// 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文件中处理点击事情,就是通过前面我们封装好的接口去处理即可

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、项目演示

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

相关推荐
轩源源12 分钟前
数据结构——红黑树的实现
开发语言·数据结构·c++·算法·红黑树·单旋+变色·双旋+变色
_DCG_38 分钟前
c++设计模式之策略模式
c++·设计模式·策略模式
自动驾驶小卡1 小时前
线性回归计算斜率原理及C++实现
c++·算法·回归·线性回归
DexterYttt2 小时前
P5788 【模板】单调栈
数据结构·c++·算法·蓝桥杯
上元星如雨3 小时前
详解std::placeholders
c++
tamak3 小时前
c/c++蓝桥杯经典编程题100道(19)质因数分解
c语言·数据结构·c++·算法·蓝桥杯
小禾苗_4 小时前
C++ ——从C到C++
开发语言·c++
暮雨哀尘5 小时前
蓝桥杯C语言组:动态规划问题
c语言·c++·算法·蓝桥杯·动态规划·线性dp
溟洵5 小时前
【C/C++算法】从浅到深学习---滑动窗口(图文兼备 + 源码详解)
c语言·c++·算法
jf加菲猫5 小时前
12 代理(Proxy)模式
c++·设计模式