通过清心醉

c++使用MFC利用std::thread多线程不阻塞

注:本文由作者原创,不可转载和抄袭,否则将追究对应的法律责任,请珍惜开发者的劳动成果。

打开VS,创建一个基于对话框的MFC应用。

文中作者创建的为“多线程Thread应用”

首先我们添加头文件

#include <thread>
#include <atomic> 
using namespace std;

然后我们需要一个防止MFC应用退出的线程和一个按钮的线程应用。

我们可以把下面的代码,写入到类里或者全局变量。因为只是说明,就不在进行更具体的说明了

atomic_int thread1_status = 0; //1已启动0未启动
atomic_int thread2_status = 0; //1已启动0未启动
atomic_int thread1_exit = 1; //1已退出,0未退出
atomic_int thread2_exit = 1; //1已退出,0未退出
//不要感觉设置thread1_exit和thread2_exit没用,因为OnBnClickedCancel事件的时候,是强制设置thread1_status和thread2_status = 0让线程退出
//OnBnClickedCancel设置之后马上就检测thread1_status和thread2_status是不是0那肯定是不行的,可能线程还在挂起处理着上下文Sleep了,可是OnBnClickedCancel已经都要执行退出了
//所以OnBnClickedCancel设置thread1_status和thread2_status为0之后,OnBnClickedCancel里的while会一直检测线程有没有正式退出。直到检测退出才全部关闭。
DWORD thread1(LPVOID lpParameter) {
	thread1_exit = 0; //只要线程启动,必须强制设置为0,这样进程OnBnClickedCancel的时候,不管线程是否启动(启动如果循环种就是0)状态,进程就不可以退出,自己可以做自己的业务逻辑
	C多线程Thread应用Dlg* pThis = (C多线程Thread应用Dlg*)lpParameter;
	while (true) {
		if(thread1_status == 1){
			//自己的业务代码
			Sleep(100);
		}
		else {
			break; //退出
		}
	}
	thread1_exit = 1; //通过OnBnClickedCancel把thread1_status 设置为0的时候,表示中止了。 这时候thread1_exit就要=1表示已经退出线程。
	return 0;
}

DWORD thread2(LPVOID lpParameter) {
	thread2_exit = 0; //只要线程启动,必须强制设置为0,这样进程OnBnClickedCancel的时候,不管线程是否启动(启动如果循环种就是0)状态,进程就不可以退出,自己可以做自己的业务逻辑
	C多线程Thread应用Dlg* pThis = (C多线程Thread应用Dlg*)lpParameter;
	while (true) {
		if (thread2_status == 1) {

                        //自己的业务代码
			pThis->MessageBox(L"线程2的状态");
			Sleep(5000);
			Sleep(100);
		}
		else {
			break; //退出
		}
	}
	thread2_exit = 1; //通过OnBnClickedCancel把thread2_status 设置为0的时候,表示中止了。 这时候thread2_exit就要=1表示已经退出线程。
	return 0;
}

好了,默认启动MFC,我们需要创建一个后台一直运作的线程,也就是所谓的挂起,不让点了确定按钮就退出。

所以我们需要在初始化里加上代码

找到该方法

BOOL C多线程Thread应用Dlg::OnInitDialog()

我们添加初始化线程创建,并且后台运行,添加到return TRUE前面即可。

	//清心醉添加,挂起的线程,防止主进程退出
	thread t1(thread1, this);
	thread1_status = 1; //让线程里的while 条件不要中止 
	t1.detach();

接下来,我们要处理MFC界面里的确定按钮的事件代码了,主要创建线程2,我们在OnBnClickedOk()方法里添加

	thread t2(thread2, this);
	thread2_status = 1; //让线程while条件不要中止
	MessageBox(L"线程2创建并且后台运行");
	t2.detach();

现在我们点确定之后,MFC应用不会退出,其实thread t1方法可以不用的,为什么我们习惯在初始换强制创建线程进行死循环,就是因为我们的按钮的事件,如果是执行一个任务(比如打开文件读取然后关闭),任务执行完成如果不是while的方式一直在挂起,主进程MFC应用就会退出。

最后,就是我们点取消退出的时候,对线程进行关闭检测。OnBnClickedCancel()方法里修改

	thread1_status = 0; //全局变量强制设置成0
	thread2_status = 0; //全局变量强制设置成0
	while (true) {
		//100毫秒检查一次是否全部已经完整退出,这里如果没有点确定按钮线程thread2_exit永远是1。就不会说一直检测不到导致死循环没响应。如果线程里Sleep或者其他阻塞任务导致卡住,那么就会一直等待响应,所以说自己要做好线程里的任务的响应
		// 最好while每一下都能在几秒内处理,这样点取消就能在几秒内效应,这里作者的thread2是Sleep(5),所以点了退出可能要延迟5秒。
		if (thread1_exit == 1 && thread2_exit == 1) {
			break;
		}
		Sleep(100);
	}
	CDialogEx::OnCancel();

最后来一个完整的代码:


// 多线程Thread应用Dlg.cpp: 实现文件
//

#include "pch.h"
#include "framework.h"
#include "多线程Thread应用.h"
#include "多线程Thread应用Dlg.h"
#include "afxdialogex.h"
#include <thread> //多线程
#include <atomic> //避免线程和进程的资源争夺,也可以考虑mutex用锁,但是效率太低了
using namespace std;
#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//使用atomic_int的方式,是如果核心业务代码中,多个线程或者线程与主进程之间需要读取修改全局的变量等数据的时候存在冲突,导致非安全线程
atomic_int thread1_status = 0; //1已启动0未启动
atomic_int thread2_status = 0; //1已启动0未启动
atomic_int thread1_exit = 1; //1已退出,0未退出
atomic_int thread2_exit = 1; //1已退出,0未退出
//不要感觉设置thread1_exit和thread2_exit没用,因为OnBnClickedCancel事件的时候,是强制设置thread1_status和thread2_status = 0让线程退出
//OnBnClickedCancel设置之后马上就检测thread1_status和thread2_status是不是0那肯定是不行的,可能线程还在挂起处理着上下文Sleep了,可是OnBnClickedCancel已经都要执行退出了
//所以OnBnClickedCancel设置thread1_status和thread2_status为0之后,OnBnClickedCancel里的while会一直检测线程有没有正式退出。直到检测退出才全部关闭。
DWORD thread1(LPVOID lpParameter) {
	thread1_exit = 0; //只要线程启动,必须强制设置为0,这样进程OnBnClickedCancel的时候,不管线程是否启动(启动如果循环种就是0)状态,进程就不可以退出,自己可以做自己的业务逻辑
	C多线程Thread应用Dlg* pThis = (C多线程Thread应用Dlg*)lpParameter;
	while (true) {
		if(thread1_status == 1){
			//自己的业务代码
			Sleep(100);
		}
		else {
			break; //退出
		}
	}
	thread1_exit = 1; //通过OnBnClickedCancel把thread1_status 设置为0的时候,表示中止了。 这时候thread1_exit就要=1表示已经退出线程。
	return 0;
}

DWORD thread2(LPVOID lpParameter) {
	thread2_exit = 0; //只要线程启动,必须强制设置为0,这样进程OnBnClickedCancel的时候,不管线程是否启动(启动如果循环种就是0)状态,进程就不可以退出,自己可以做自己的业务逻辑
	C多线程Thread应用Dlg* pThis = (C多线程Thread应用Dlg*)lpParameter;
	while (true) {
		if (thread2_status == 1) {
			//自己的业务代码
			pThis->MessageBox(L"线程2的状态");
			Sleep(5000);
		}
		else {
			break; //退出
		}
	}
	thread2_exit = 1; //通过OnBnClickedCancel把thread2_status 设置为0的时候,表示中止了。 这时候thread2_exit就要=1表示已经退出线程。
	return 0;
}

// 用于应用程序“关于”菜单项的 CAboutDlg 对话框

class CAboutDlg : public CDialogEx
{
public:
	CAboutDlg();

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

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

// 实现
protected:
	DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()


// C多线程Thread应用Dlg 对话框



C多线程Thread应用Dlg::C多线程Thread应用Dlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_THREAD_DIALOG, pParent)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void C多线程Thread应用Dlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(C多线程Thread应用Dlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDOK, &C多线程Thread应用Dlg::OnBnClickedOk)
	ON_BN_CLICKED(IDCANCEL, &C多线程Thread应用Dlg::OnBnClickedCancel)
END_MESSAGE_MAP()


// C多线程Thread应用Dlg 消息处理程序

BOOL C多线程Thread应用Dlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 将“关于...”菜单项添加到系统菜单中。

	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != nullptr)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO: 在此添加额外的初始化代码
	//清心醉添加,挂起的线程,防止主进程退出
	thread t1(thread1, this);
	thread1_status = 1; //让线程里的while 条件不要中止 
	t1.detach();
	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

void C多线程Thread应用Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
		CAboutDlg dlgAbout;
		dlgAbout.DoModal();
	}
	else
	{
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

// 如果向对话框添加最小化按钮,则需要下面的代码
//  来绘制该图标。  对于使用文档/视图模型的 MFC 应用程序,
//  这将由框架自动完成。

void C多线程Thread应用Dlg::OnPaint()
{
	if (IsIconic())
	{
		CPaintDC dc(this); // 用于绘制的设备上下文

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// 使图标在工作区矩形中居中
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// 绘制图标
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
		CDialogEx::OnPaint();
	}
}

//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR C多线程Thread应用Dlg::OnQueryDragIcon()
{
	return static_cast<HCURSOR>(m_hIcon);
}



void C多线程Thread应用Dlg::OnBnClickedOk()
{
	thread t2(thread2, this);
	thread2_status = 1; //让线程while条件不要中止
	MessageBox(L"线程2创建并且后台运行");
	t2.detach();
	// TODO: 在此添加控件通知处理程序代码
	//CDialogEx::OnOK();
}


void C多线程Thread应用Dlg::OnBnClickedCancel()
{
	// TODO: 在此添加控件通知处理程序代码
	thread1_status = 0; //全局变量强制设置成0
	thread2_status = 0; //全局变量强制设置成0
	while (true) {
		//100毫秒检查一次是否全部已经完整退出,这里如果没有点确定按钮线程thread2_exit永远是1。就不会说一直检测不到导致死循环没响应。如果线程里Sleep或者其他阻塞任务导致卡住,那么就会一直等待响应,所以说自己要做好线程里的任务的响应
		// 最好while每一下都能在几秒内处理,这样点取消就能在几秒内效应,这里作者的thread2是Sleep(5),所以点了退出可能要延迟5秒。
		if (thread1_exit == 1 && thread2_exit == 1) {
			break;
		}
		Sleep(100);
	}
	CDialogEx::OnCancel();
}

最后总结以下,如果只是WIN应用的话,感觉还是CreateThread的方式会更好,自己可以执行挂起和关闭退出动作。

如果是用于LINUX等跨平台的,还是thread最好。

而且使用MFC的方式,是能更好的表现出线程在正常的挂起在后台运行,直到OnBnClickedCancel()在检测。

还有一个,比如文中的t2线程,如果里面有很多的任务,比如有while(true)的死循环任务(比如TCP服务端或者其他的轮询服务)

特别是在Sleep特别多的情况下,建议自己封装一个方法来实现Sleep检测状态是否要设置false和退出

还有状态和退出的全局变量,如果是多个线程,甚至单个线程和主进程之间可能存在着资源的争夺,使用atomic或者mutex锁等等。因为join会阻塞进程等待线程完成,所以在线程方法里如果有while是不可行的,而这里使用的是detach之后主进程实现无阻塞的进行正常执行和退出,当然虽然说不阻塞但是还是会受线程或者主进程里的 sleep影响,如果Sleep控制得当,基本x秒内响应线程正常关闭是没压力。

关于作者

清心醉 administrator

发表评论

请输入验证码: