作者:小蜗牛向前冲
名言:我可以接受失败,但我不能接受放弃
如果觉的博主的文章还不错的话,还请
点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正
目录
[3.2、通过类封装 线程操作](#3.2、通过类封装 线程操作)
一、项目解析
![](https://i-blog.csdnimg.cn/direct/d8268415d2aa47a6bf8a22c35fa6352f.png)
这里我们就要控制线程对临界资源的访问,通过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
用于保护共享资源,确保同一时刻只有一个线程能够进入临界区。
- 初始化临界区
cpp
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
lpCriticalSection:指向一个 CRITICAL_SECTION
结构的指针,系统会根据这个结构初始化临界区。
- 删除临界区
当不再需要临界区时,应该调用 DeleteCriticalSection
来销毁它。这样可以释放与临界区相关的资源。
cpp
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
- 加锁临界区
使用 EnterCriticalSection
函数来加锁临界区。当一个线程试图访问临界区时,它必须首先请求锁。如果锁已经被其他线程持有,则该线程将会被阻塞,直到锁变得可用。
cpp
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
- 解锁临界区
使用 LeaveCriticalSection
来解锁临界区,这样其他线程可以访问临界区中的资源。
cpp
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
要使用临界资源,我们要先定义应该临界区,比如: CRITICAL_SECTION m_critical_section对象
,然后调用相应的函数就好了。如EnterCriticalSection(&m_CriticalSection);
三、项目实现
为了实现这个项目,我们通过MFC创建应该基于对话框的项目。
3.1、在Dialg资源UI控制好界界面
这里要注意为对话框添加变量名,因为我要显示线程运行的数据在上面。
![](https://i-blog.csdnimg.cn/direct/dd221f33de974f52b92b91d472b6d655.png)
这里我们在对话框的.h头文件中可以看到
![](https://i-blog.csdnimg.cn/direct/8b99568e349f467cadf39f6d05cea16c.png)
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、项目演示
这里我们点击启动线程,就可以观察到对话框的变化。
![](https://i-blog.csdnimg.cn/direct/6a56829b3d6d4bb7b789168718aed261.png)