ThinkNotes

Simple is not easy | 化繁为简,知易行难

0%

MFC笔记:多线程磁盘读写测试工具

前言

MFC(Microsoft Foundation Classes)是微软在win32 API上,用C++封装的GUI框架,在现在,MFC相比其他的GUI框架有些过时,可以参考:
很多人说 C++ 的 MFC 已经过时了,那新入门的人到底应该学什么?
不同环境的选择:

  • 跨平台: QT
  • C#: WPF
  • Web:React,Vue,Electron

既然如此,为何本文用MFC?
1.部分功能从老MFC项目移植,且VS环境能快速上手
2.技术本身不会过时,过时的是应用场景,GUI回调式的交互机制,以及Win32线程和进程的使用都是通用的技术。这是写本文的原因

本文源码:cursorhu/myMFCForAutoRWTest

GUI界面:
1

初识MFC项目

VS新建MFC项目,例如“myMFC”,目录结构如下
2
myMFC.cpp是VS自动创建的MFC项目入口,其主要功能是:创建一个窗口实例,注册会话对象(Dialog)
界面的交互一定是分层的

  • 对用户的是控件层,即各种按钮,输入输出框等可见可操作的东西
  • 处理数据的是逻辑层,例如从输入框输入,底层保存该字符串,点击运行,底层开始执行对应函数

在MFC中,会话对象就是处理底层逻辑的类对象,其方法定义在myMFCDlg.cpp
也是开发的主要内容

MFC入口

下面介绍myMFC.cpp的MFC入口:

BOOL CmyMFCApp::InitInstance()
{
	// 如果一个运行在 Windows XP 上的应用程序清单指定要
	// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
	//则需要 InitCommonControlsEx()。  否则,将无法创建窗口。
	INITCOMMONCONTROLSEX InitCtrls;
	InitCtrls.dwSize = sizeof(InitCtrls);
	// 将它设置为包括所有要在应用程序中使用的
	// 公共控件类。
	InitCtrls.dwICC = ICC_WIN95_CLASSES;
	InitCommonControlsEx(&InitCtrls);

	CWinApp::InitInstance();
	
	AfxEnableControlContainer();

	// 创建 shell 管理器,以防对话框包含
	// 任何 shell 树视图控件或 shell 列表视图控件。
	CShellManager *pShellManager = new CShellManager;

	// 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题
	CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));

	// 标准初始化
	// 如果未使用这些功能并希望减小
	// 最终可执行文件的大小,则应移除下列
	// 不需要的特定初始化例程
	// 更改用于存储设置的注册表项
	// TODO: 应适当修改该字符串,
	// 例如修改为公司或组织名
	SetRegistryKey(_T("应用程序向导生成的本地应用程序"));

	CmyMFCDlg dlg;
	m_pMainWnd = &dlg;
	INT_PTR nResponse = dlg.DoModal();
	if (nResponse == IDOK)
	{
		// TODO: 在此放置处理何时用
		//  “确定”来关闭对话框的代码
	}
	else if (nResponse == IDCANCEL)
	{
		// TODO: 在此放置处理何时用
		//  “取消”来关闭对话框的代码
	}
	else if (nResponse == -1)
	{
		TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n");
		TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n");
	}

	// 删除上面创建的 shell 管理器。
	if (pShellManager != nullptr)
	{
		delete pShellManager;
	}

#if !defined(_AFXDLL) && !defined(_AFX_NO_MFC_CONTROLS_IN_DIALOGS)
	ControlBarCleanUp();
#endif

	return FALSE;
}

只需要关注这几句

CmyMFCDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();

CmyMFCDlg类是在myMFCDlg.cpp定义的,即底层逻辑类。m_pMainWnd是myMFC.cpp的CmyMFCApp类(继承win32 API)的成员,表示主窗口,这两句就是把会话对象注册到窗口类,这样窗口运行时可以回调会话对象的方法。dlg.DoModal()是运行会话窗口,运行哪个会话?其调用者CmyMFCDlg类对象dlg。

MFC逻辑层

VS自动创建myMFC项目的会话逻辑层,myMFCDlg.cpp
几个自动生成的方法如下,这里为了作为示例,加了自定义的类成员m_src, m_dst和方法OnBnClickedButtonsrc,OnBnClickedButtondst
(1)会话类构造函数

CmyMFCDlg::CmyMFCDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_MYMFC_DIALOG, pParent)
	, m_src(_T("")) //初始化为空串,_T是兼容不同编码的转换
    , m_dst(_T(""))
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

(2)界面和类成员数据关联

void CmyMFCDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Text(pDX, IDC_EDIT_src, m_src); //关联m_src和IDC_EDIT_src控件,该控件是界面输入框
	DDX_Text(pDX, IDC_EDIT_dst, m_dst);
}

(3)界面和类方法的关联

BEGIN_MESSAGE_MAP(CmyMFCDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDC_BUTTON_src, &CmyMFCDlg::OnBnClickedButtonsrc) //关联IDC_BUTTON_src按钮和OnBnClickedButtonsrc方法
	ON_BN_CLICKED(IDC_BUTTON_dst, &CmyMFCDlg::OnBnClickedButtondst)
END_MESSAGE_MAP()

类在头文件的定义:

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

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_MYMFC_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()
	
public:
	CString m_src; //CString: MFC的字符串类型
	CString m_dst;
	afx_msg void OnBnClickedButtonsrc(); //afx_msg: MFC的方法对应的消息响应类型
	afx_msg void OnBnClickedButtondst();
};

在VS环境下,这些变量和方法的定义都不需要写代码,在控件资源视图直接配置即可。

界面资源层

注意项目文件有个Resource.h,包含界面相关的资源,如每个按钮有个ID,这个不要手动配置,在编辑UI控件时自动生成

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 myMFC.rc 使用
//
#define IDM_ABOUTBOX                    0x0010
#define IDD_ABOUTBOX                    100
#define IDS_ABOUTBOX                    101
#define IDD_MYMFC_DIALOG                102
#define IDR_MAINFRAME                   128
#define IDC_BUTTON_src                  1000
#define IDC_BUTTON_dst                  1001

myMFC.rc是UI的资源文件,打开就是UI界面
3
4
可以看到界面的按钮,右键查看属性,可以修改标题和控件ID,会映射到Resource.h。双击按钮,myMFCDlg.cpp会自动创建方法CmyMFCDlg::OnBnClickedButtondst(),头文件自动加方法声明。
5

前文的Dlg.cpp中的控件ID, dlg类的方法,变量,从一开始就可以从资源界面配置,自动生成:

  • 在资源界面选按钮或其他控件
  • 右键配置控件ID
  • 右键添加值变量或控件变量
  • 双击添加方法

6
关于值变量和控件变量:
值变量用于关联界面和类成员,值变量就是类成员名,例如点击dst按钮调用其方法后,获得的路径,会写入m_dst值变量
7
控件变量代表控件本身,用于底层逻辑中,直接调用控件的方法,例如控件变量叫dst_ctrl,可以在某个方法中ctrl_dst.SetWindowText(_T(""))清空界面的字符串
8

简单拷贝校验的实现

实现从src目录拷贝所有文件到dst目录,并比较拷贝前后的文件是否一致

获取文件路径

两个路径选择按钮和对应的编辑框显示路径,一个Start按钮
9

button src的方法:

void CmyMFCDlg::OnBnClickedButtonsrc()
{
	CString SrcPath;
	SrcPath = GetFolderPath(); //获取文件夹路径
	ctrl_src.SetWindowText(SrcPath); //显示获取的路径字符串
	m_src = SrcPath; //保存路径到会话对象的变量
}

GetFolderPath打开一个目录框,让用户选择:
SHBrowseForFolder是win32 API,专用于打开目录

CString CmyMFCDlg::GetFolderPath(void)
{
	CString strPath;
	BROWSEINFO bInfo;
	ZeroMemory(&bInfo, sizeof(bInfo));
	bInfo.hwndOwner = m_hWnd;
	bInfo.lpszTitle = _T("Select Folder: ");
	bInfo.ulFlags = BIF_RETURNONLYFSDIRS;

	LPITEMIDLIST lpDlist;					
	lpDlist = SHBrowseForFolder(&bInfo); //win32 API, 打开目录	
	if (lpDlist != NULL)						
	{
		TCHAR chPath[255];					
		SHGetPathFromIDList(lpDlist, chPath);
		strPath = chPath;					
	}
	return strPath;
}

如果是打开文件,用CFileDialog

CString CmyMFCDlg::GetFilePath(void)
{
	CFileDialog mFileDlg(TRUE, NULL, NULL,
		OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_ALLOWMULTISELECT | OFN_NOCHANGEDIR,
		_T("All Files(*.*)|*.*||"), AfxGetMainWnd());
	CString str(" ", 10000);
	mFileDlg.m_ofn.lpstrFile = str.GetBuffer(10000);
	mFileDlg.m_ofn.lpstrTitle = _T("Select File");
	str.ReleaseBuffer();
	mFileDlg.DoModal();
	POSITION mPos = mFileDlg.GetStartPosition();
	CFileStatus status;
	CString strPath;
	while (mPos != NULL)
	{
		strPath = mFileDlg.GetNextPathName(mPos);
		CFile::GetStatus(strPath, status);
	}
	return strPath;
}

不管哪一种,效果如下
10
选择完后,路径会在编辑框显示,这就是控件语句ctrl_src.SetWindowText(SrcPath)的效果
11

拷贝和比较

拷贝函数如下,只需关注几个函数:

  • CFileFind类的CFileFind(), FindNextFile(), GetFilePath(), GetFilePath(),这些都是afx.h定义,属于MFC库的类
  • CopyFile(), 执行拷贝,这个也是继承自MFC类

代码:

BOOL CmyMFCDlg::ModeTestCopyFileFromSRCtoDST(CString SRC, CString DST, CString& StrResult)
{

	CFileFind ff, ff_DST;
	CString SRCDir = SRC;                 //source folder path
	CString DSTDir = DST;
	UINT copyFileResult = 0;
	int i = 0;

	BOOL bmakedir = MakeDirectory(DSTDir);

	if (SRCDir.Right(1) != _T("\\"))
		SRCDir += _T("\\");
	SRCDir += _T("*.*");

	if (DSTDir.Right(1) != _T("\\"))
		DSTDir += _T("\\");


​ SetLastError(0);
​ CString DST_tmp = DSTDir + _T(“.“);
​ BOOL res_DST = ff_DST.FindFile(DST_tmp);
​ if (res_DST == 0)
​ {
​ StrResult.Format(_T(“Access DST folder error, error code is %d. “), GetLastError());
​ }
​ BOOL res = ff.FindFile(SRCDir);

​ while (res)
​ {
​ res = ff.FindNextFile();
​ if (!ff.IsDirectory() && !ff.IsDots())
​ {
​ CString DSTFildPath;
​ CString SRCFilePath = ff.GetFilePath();
​ DSTFildPath = DSTDir + ff.GetFileName();
​ copyFileResult = CopyFile(ff.GetFilePath(), DSTFildPath, FALSE);

​ Sleep(2000);

if (copyFileResult == 0)
{
DWORD ErrCode = GetLastError();
StrResult.Format(_T(“CopyFile failed! The ErrCode is %d. “), ErrCode);

				for (i = 0; i < 10; i++)
				{
					copyFileResult = CopyFile(ff.GetFilePath(), DSTFildPath, FALSE);
					Sleep(2000);
					if (copyFileResult == 0)
					{
						ErrCode = GetLastError();
						StrResult.Format(_T("Retry CopyFile failed! The ErrCode is %d. "), ErrCode);
					}
					else
					{
						break;
					}
				}

				if (copyFileResult == 0)
				{
					ff.Close();
					return FALSE;
				}
			}
		}
		else if (ff.IsDirectory() && !ff.IsDots())
		{
			CString DSTFildPath;
			DSTFildPath = DSTDir + ff.GetFileName();
			copyFileResult = ModeTestCopyFileFromSRCtoDST(ff.GetFilePath(), DSTFildPath, StrResult);
			if (copyFileResult == 0)
				break;
		}
	}

	ff.Close();
	if (copyFileResult == 0)
		return FALSE;
	else
		return TRUE;
}

比较两个路径的文件:
其方法是,文件读到buffer, 再用memcmp比较buffer, 其FindNextFile也是如何从目录搜索到文件的关键方法

BOOL CmyMFCDlg::ModeTestCompareFilesBetweenSRCandDST(CString SRC, CString DST, CString& StrResult)
{
	CFileFind ff;
	CString SRCDir = SRC;
	CString DSTDir = DST;
	BOOL bRes = TRUE;
	HANDLE hSrcFile, hDstFile;
	DWORD dwSRCFile, dwDSTFile, dwCB;

	if (SRCDir.Right(1) != _T("\\"))
		SRCDir += _T("\\");
	SRCDir += _T("*.*");

	if (DSTDir.Right(1) != _T("\\"))
		DSTDir += _T("\\");
	hSrcFile = hDstFile = NULL;
	BYTE* pSrcBuffer = new BYTE[M_BUFSIZE];
	BYTE* pDstBuffer = new BYTE[M_BUFSIZE];
	memset(pSrcBuffer, 0, M_BUFSIZE);
	memset(pSrcBuffer, 0, M_BUFSIZE);

	BOOL res = ff.FindFile(SRCDir);

	while (res)
	{
		res = ff.FindNextFile();
		if (!ff.IsDirectory() && !ff.IsDots())
		{
			CString DSTFilePath;
			DSTFilePath = DSTDir + ff.GetFileName();
			CString SRCFilePath = ff.GetFilePath();

			if (hSrcFile)
			{
				CloseHandle(hSrcFile);
				hSrcFile = NULL;
			}

			if (hDstFile)
			{
				CloseHandle(hDstFile);
				hSrcFile = NULL;
			}

			hSrcFile = CreateFile(SRCFilePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
			if (hSrcFile == INVALID_HANDLE_VALUE)
			{
				StrResult.Format(_T("\n Create Source file failed!! Error code = %d \n"), GetLastError());
				bRes = FALSE;
				break;
			}

			hDstFile = CreateFile(DSTFilePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
			if (hDstFile == INVALID_HANDLE_VALUE)
			{
				StrResult.Format(_T("\n Create Destination file failed!! Error code = %d \n"), GetLastError());
				bRes = FALSE;
				break;
			}

			LARGE_INTEGER SrcFileSize, DstFileSize;

			dwSRCFile = GetFileSizeEx(hSrcFile, &SrcFileSize);
			dwDSTFile = GetFileSizeEx(hDstFile, &DstFileSize);

			if (SrcFileSize.LowPart != DstFileSize.LowPart)
			{
				StrResult.Format(_T("\n Compare file is different!! Src Length = %d, Dest Length = %d \n"), SrcFileSize.LowPart, DstFileSize.LowPart);
				bRes = FALSE;
				break;
			}

			while (SrcFileSize.LowPart > 0)
			{
				BOOL bCmpResult;
				bCmpResult = ReadFile(hSrcFile, pSrcBuffer, M_BUFSIZE, &dwCB, NULL);
				if (bCmpResult == 0)
				{
					bRes = FALSE;
					break;
				}
				bCmpResult = ReadFile(hDstFile, pDstBuffer, M_BUFSIZE, &dwCB, NULL);
				if (bCmpResult == 0)
				{
					bRes = FALSE;
					break;
				}
				bCmpResult = memcmp(pSrcBuffer, pDstBuffer, dwCB);

				if (bCmpResult != 0)
				{
					bRes = FALSE;
					CString DiffByte;

					CString PostCmpErrorStr;
					CString SrcDumpData, DstDumpData;
					StrResult.Format(_T("\n Fatal_Error: Src Data from %d to %d.\n"), (DstFileSize.LowPart - SrcFileSize.LowPart), (DstFileSize.LowPart - SrcFileSize.LowPart + dwCB));

					PostCmpErrorStr = _T("SourceFilePath: ") + SRCFilePath + _T(" To \r\n") + _T("DstFilePath: ") + DSTFilePath;
					StrResult = PostCmpErrorStr + _T("  has compare error! \r\n");
					//HugoPostMessageAndShowSD1(PostCmpErrorStr,1);
					//HugoPostMessageAndShowSD2(PostCmpErrorStr,1);

					::MessageBox(
						NULL,
						(LPCWSTR)L"Compare error happened!!",
						(LPCWSTR)L"Fatal Error!",
						MB_OK
					);

					break;
				}
				SrcFileSize.LowPart -= dwCB;
			}

			if (bRes == FALSE)
				break;
			else
				ReadFile(hDstFile, pDstBuffer, 512, &dwCB, NULL);
		}
		else if (ff.IsDirectory() && !ff.IsDots())
		{
			CString DSTFildPath;
			DSTFildPath = DSTDir + ff.GetFileName();
			bRes = ModeTestCompareFilesBetweenSRCandDST(ff.GetFilePath(), DSTFildPath, StrResult);
			if (bRes == FALSE)
				break;
		}
	}

	if (hSrcFile)
	{
		CloseHandle(hSrcFile);
		hSrcFile = NULL;
	}

	if (hDstFile)
	{
		CloseHandle(hDstFile);
		hSrcFile = NULL;
	}

	if (bRes == FALSE)
	{
		delete[]pSrcBuffer;
		delete[]pDstBuffer;
		ff.Close();
		return FALSE;
	}
	else
	{
		delete[]pSrcBuffer;
		delete[]pDstBuffer;
		ff.Close();
		return TRUE;
	}
}

关于CString的格式化输出:MFC中CString.Format的详细用法
关于CFile文件操作:MFC——文件操作(CFile)

开始按钮

一般操作顺序:选择src和dst,再点击Start按钮
start按钮的方法调用已保存的m_src和m_dst路径,传入拷贝和比较,再输出结果即可,大致流程如下

void CmyMFCDlg::OnBnClickedButtonrun()
{
    //读入所有界面数据
	UpdateData(true);
	
	BOOL ret;
	CString outStr;
	
	ret = ModeTestCopyFileFromSRCtoDST(m_src, m_dst, outStr);
	if (!ret)
			MessageBox(NULL, _T(outStr), _T("ERROR"), MB_OK);
	
	ret = ModeTestCompareFilesBetweenSRCandDST(m_src, m_dst, outStr);
	if (!ret)
			MessageBox(NULL, _T(outStr), _T("ERROR"), MB_OK);
}

这里用messagebox输出结果,即弹窗,弹窗是阻塞式的。也可以用编辑框,写文件输出。
关于messagebox,参考:MessageBox function (winuser.h)
关于updateData:MFC中UpdateData()函数的使用
以上完成一个简单的文件拷贝和比较功能

多线程文件拷贝和写日志

将简单拷贝扩展,支持:

  • 多线程拷贝和比较,每个线程完成简单拷贝比较的功能
  • 在每个工作线程,输出打印到界面文本框,同时写到同一个日志文件
  • 界面主线程需要等待所有工作线程完成后,输出测试完成信息到文本框和日志

线程列表获取各自路径

add和delete配置几个工作线程,每个线程配置其src和dst路径

12

13

14

这种动态增删的列表,在资源界面新建listbox类型变量和方法:

CListBox m_rwlist;
afx_msg void OnLbnSelchangeListrwlist();

Add和Delete对应的方法:

void CmyMFCDlg::OnBnClickedButtonadd()
void CmyMFCDlg::OnBnClickedButtondelete()

Add和Delete的方法控制listbox变量m_rwlist,选中任意m_rwlist后又会调用其方法OnLbnSelchangeListrwlist,获取每个线程各自的src、dst。

按键控制m_rwlist的实现:

void CmyMFCDlg::OnBnClickedButtonadd()
{
	CString Threadtest = _T("TestThread");
	UINT ThreadCount = m_rwlist.GetCount();
	if (ThreadCount == 0)
	{
		m_rwlist.AddString(_T("TestThread1"));
	}
	else if (ThreadCount < MAX_THREAD_COUNT)
	{
		CString ThreadNum;
		ThreadNum.Format(_T("%d"), ThreadCount + 1);
		Threadtest = Threadtest + ThreadNum;
		m_rwlist.AddString(Threadtest);
	}
	else if (ThreadCount == MAX_THREAD_COUNT)
	{
		CString str;
		str.Format(_T("Only support %d threads at most!!"), MAX_THREAD_COUNT);
		MessageBox(str);
	}
	m_rwlist.SetCurSel(ThreadCount);
	if (ThreadCount < MAX_THREAD_COUNT)
		totalThreadCount++;
}

void CmyMFCDlg::OnBnClickedButtondelete()
{
	UINT ThreadCount = m_rwlist.GetCount();
	if (ThreadCount != 0)
	{
		m_rwlist.DeleteString(ThreadCount - 1);
		m_rwlist.SetCurSel(0);
	}
	if (ThreadCount > 0)
		totalThreadCount--;
}

线程列表m_rwlist的方法读取路径到会话对象成员变量:

void CmyMFCDlg::OnLbnSelchangeListrwlist()
{
	UpdateData(true); //update true: 从界面读入值到变量(使上次编辑生效)
	if (m_rwlist.GetCount() != 0)
	{
		UINT selectNum = m_rwlist.GetCurSel();
		RWTestParamArray[selectNum].ThreadNum = m_rwlist.GetCount();
		RefreshRWParam(RWTestParamArray, selectNum);
	}
}

void CmyMFCDlg::RefreshRWParam(TabDialogRWTestParam(&Array)[MAX_THREAD_COUNT], UINT CSel)
{
	ctrl_src.SetWindowText(Array[CSel].SRCFolder_Path);
	ctrl_dst.SetWindowText(Array[CSel].DSTFolder_Path);

	UpdateData(false); //update false: 把变量写入到界面(实时显示)
}

真正读入路径的是dst、src按钮的方法:

void CmyMFCDlg::OnBnClickedButtonsrc()
{
	CString SrcPath;
	UINT ThreadCSelNum = m_rwlist.GetCurSel();
	SrcPath = GetFolderPath();
	ctrl_src.SetWindowText(SrcPath);
	RWTestParamArray[ThreadCSelNum].SRCFolder_Path = SrcPath;
}

void CmyMFCDlg::OnBnClickedButtondst()
{
	CString DstPath;
	UINT ThreadCSelNum = m_rwlist.GetCurSel();
	DstPath = GetFolderPath();
	ctrl_dst.SetWindowText(DstPath);
	RWTestParamArray[ThreadCSelNum].DSTFolder_Path = DstPath;
}

线程数组定义在会话类,存储每个工作线程要用的数据

TabDialogRWTestParam RWTestParamArray[MAX_THREAD_COUNT];
typedef struct TabRWParam
{
	CString SRCFolder_Path;
	CString DSTFolder_Path;
	UINT ThreadNum;
	UINT TestTimes;
}TabDialogRWTestParam;

创建线程

创建线程参考MS文档:beginthread、_beginthreadex
关注2点:

  • 传入线程内要执行的函数,和参数(可为NULL)
  • 返回线程句柄,如果是多个线程则是个数组

创建线程的部分代码:

void CmyMFCDlg::RunModeTestInstance()
{
    ....
    
	//线程内除了对象,还需要知道自己是哪个线程,因此打包this和ThreadCount
	pTransParam ThreadTransPArray[MAX_THREAD_COUNT];

	for (int i = 0; i < totalThreadCount; i++)
	{
		ThreadTransPArray[i] = new(TransParam);
		ThreadTransPArray[i]->i = i;
		ThreadTransPArray[i]->translpParam = this;

		unsigned int rwThreadID;

		//hThread defined as global data
		hThread[i] = (HANDLE)_beginthreadex(
			NULL,
			0,
			DoThreadProc,
			ThreadTransPArray[i],
			0,
			&rwThreadID);

		if (hThread[i] == NULL)
			MessageBox(_T("CreateThread Fail!!"), MB_OK);
		
		....
		
		//release resource
    	for (int i = 0; i < totalThreadCount; i++)
    	{
    		delete ThreadTransPArray[i];
    		ThreadTransPArray[i] = NULL;
    		CloseHandle(hThread[i]);
    	}
	}

由于要在线程内打印当前是哪个线程,这个从Dlg对象的this指针是获取不到的,因此把this指针和线程id打包结构体,传入DoThreadProc线程函数,结构体如下

typedef struct transParam
{
	LPVOID translpParam;
	int i;
}TransParam, *pTransParam;

#define MAX_THREAD_COUNT 6

注意使用完后释放线程句柄和其他相关资源

主线程和工作线程的通信:Message机制

先明白几点:

  • 所有工作线程都共享主线程(界面线程)的数据,即会话类对象的成员
  • 界面控件的操作函数,都是主线程独有的,工作线程不能调用
  • 主线程如果要等待工作线程,一般会阻塞

问题:
如何将工作线程的打印输出到主线程界面控件?

Windows消息机制可以解决工作线程和主线程通信问题,简单的讲,主线程有消息队列,工作线程可以发送消息到消息队列中,主线程用FIFO原则处理队列中的消息,在阻塞等待动作线程时,也支持消息队列的处理。
关于消息队列:windows消息机制(MFC)

(1)工作线程函数

unsigned int WINAPI DoThreadProc(void *threadTransParam)
{
	pTransParam pTrans = (pTransParam)threadTransParam;
	CString strResult;
	BOOL res = 0;

	CmyMFCDlg* pDlg = (CmyMFCDlg *)pTrans->translpParam;
	int thread_id = pTrans->i;

	for (int i = 0; i < (int)pDlg->rwtime; i++)
	{
		
		res = pDlg->ModeTestCopyFileFromSRCtoDST(pDlg->RWTestParamArray[thread_id].SRCFolder_Path, pDlg->RWTestParamArray[thread_id].DSTFolder_Path, strResult);

		res = pDlg->ModeTestCompareFilesBetweenSRCandDST(pDlg->RWTestParamArray[thread_id].SRCFolder_Path, pDlg->RWTestParamArray[thread_id].DSTFolder_Path, strResult);
		if (res)
		{
			criticalSec.Lock();
			::PostMessage(pDlg->GetSafeHwnd(), WM_USER_MSG, WPARAM(thread_id + 1), LPARAM(i + 1));
			criticalSec.Unlock();
		}
	}

	return res;
}

几点说明:

  • 线程函数要用WINAPI实现,不属于会话类内的方法,因此需要this指针显式调用
  • rwtime是测试次数,每个线程执行多次拷贝比较
  • PostMessage是发布消息到主线程消息队列,可以传参:WPARAM和LPARAM
  • 由于不确定PostMessage是不是线程安全,这里加了锁:CCriticalSection类型的criticalSec

(2)消息处理函数
来看message处理函数:

LRESULT CmyMFCDlg::OnMsg(WPARAM wp, LPARAM lp)
{
	strAppend.Format(_T("Thread %d src:%s ---> des:%s, Copy&Compare Pass: test loop: %d \n"), wp, RWTestParamArray[wp-1].SRCFolder_Path, RWTestParamArray[wp-1].DSTFolder_Path, lp);
	ShowLogInEditBox(); //字符串显示到界面
	return 0;
}

主界面字符串显示函数

/* call by message handler, for multiple child thread*/
void CmyMFCDlg::ShowLogInEditBox()
{
	CString str;
	UINT i;

	/*message 队列只在主线程内处理,无需加锁*/
	//criticalSec.Lock();
		WriteLogFile(this->strAppend); //only write append str
	//criticalSec.Unlock();

	this->GetDlgItemText(IDC_EDIT_logbox, str);
	str += this->strAppend; //update old+append str
	str += "\r\n"; //这里换行没用,要在控件设置中允许换行

	this->SetDlgItemText(IDC_EDIT_logbox, str);

	i = ((CEdit*)GetDlgItem(IDC_EDIT_logbox))->GetLineCount();
	((CEdit*)GetDlgItem(IDC_EDIT_logbox))->LineScroll(++i, 0); //定位到下一行

}

写日志的相关方法如下:

BOOL CmyMFCDlg::CreateLogFile()
{
	CString strName;
	SYSTEMTIME st;

	GetLocalTime(&st);
	strName.Format(_T("UtilityLogFile_%4d-%d-%d_%d-%d-%d.log"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);

	if (!m_File.Open(strName, (CFile::modeCreate | CFile::modeReadWrite), 0))
	{
		::AfxMessageBox(_T("Create Utility Log File Error!!"));
		return FALSE;
	}

	m_logCreated = 1;
	return TRUE;
}

void CmyMFCDlg::WriteLogFile(CString str)
{
	BOOL CreateRes = TRUE;

	if (m_logCreated == 0)
		CreateRes = CreateLogFile();

	if (CreateRes)
	{
		str += _T("\r\n");
		int length = str.GetLength();
		length *= 2;
		m_File.Write(str, length);
		m_File.Flush();
	}
}

void CmyMFCDlg::CloseLogFile()
{
	if (m_logCreated == 1)
	{
		m_File.Close();
		m_logCreated = 0;
	}
}

注意message处理函数的关键点:

  • 只在主线程中处理,不存在其他线程操作,无临界区问题。因此上述的窗口输出,日志文件写入都是线程安全的。

编辑框作为输出要注意几点:

  • 换行要在设置里配置,字符串换行没用
  • 设置输出滚动显示

效果如下:
15

(3)线程同步
日志完成的输出是主线程等待所有工作线程函数返回后才执行,如何实现?
参考:WaitForMultipleObject与MsgWaitForMultipleObjects用法
我们的需求是主线程在阻塞等待时要处理消息,因此用MsgWaitForMultipleObjects方法。
代码如下:

//wait all child threads return

	/*
	//主线程阻塞,不能处理消息
	DWORD dwWaitResult = WaitForMultipleObjects(
		totalThreadCount,
		hThread,
		TRUE,
		INFINITE);
	*/
	//主线程阻塞,但不阻塞消息
	int nWaitCount = totalThreadCount;
	int nExitThreadCount = 0;      //标记已经有几个线程退出了
	BOOL bWaitAll = FALSE;		//不等待所有线程完成,实时处理。如果TRUE, 会阻塞到所有线程完成
	DWORD result;
	MSG msg;

	while (TRUE)
	{
		/*该函数等待:多个线程的完成信号,或其他消息信号,有任意一种就返回
		*返回值为[WAIT_OBJECT_0, WAIT_OBJECT_0 + nWaitCount - 1]表示对应下标的线程已完成
		*返回值为WAIT_OBJECT_0 + nWaitCount表示有其他信号,如线程内发送的message
		*WAIT_OBJECT_0值为0
		*/
		result = MsgWaitForMultipleObjects(nWaitCount, hThread, bWaitAll, INFINITE, QS_ALLINPUT);

		if (result == WAIT_OBJECT_0 + nWaitCount) //表示收到消息
		{
			while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) //处理所有已入队的消息
			{
				TranslateMessage(&msg); //message translat and format, add into message queue
				DispatchMessage(&msg); //call message handler
			}
		}
		else if (result >= WAIT_OBJECT_0 && result < WAIT_OBJECT_0 + nWaitCount) //表示收到了线程结束信号
		{
			nExitThreadCount++;
			if (nExitThreadCount < totalThreadCount)
			{
				/*必须更新hThread,否则已退出的线程一直被检测到*/
				int nIndex = result - WAIT_OBJECT_0; //退出线程的index
				hThread[nIndex] = hThread[nWaitCount - 1]; //更新等待列表:hThread, 交换退出的成员和尾部成员
				hThread[nWaitCount - 1] = NULL;

				nWaitCount--; //更新要等待的线程数
			}
			else
			{
				break; //等待的所有线程都已完成
			}
		}
	}

	//All threads returned

MsgWaitForMultipleObjects的MS说明文档:MsgWaitForMultipleObjects function (winuser.h)
返回值的含义是重点,这个文档说的很隐晦:
16

大意是:等待n个线程

  • 如果返回的值i是属于0~n-1,说明第i个工作线程结束了
  • 如果返回值是n,不是线程结束,而是收了到消息,例如工作线程内发送的消息。

因此代码逻辑是:
1.如果有消息,就处理消息
关于message的peek,translate和dispatch:
PeekMessage使用方法
消息循环中TranslateMessage和Dispatch函数的作用

2.如果有线程结束,要更新线程句柄数组,只保留未等待到的线程;
当所有线程都等待到,退出等待循环

以上完成了主线程和多个工作线程的同步机制

再进一步:调用其他进程

现需求如下:
有多个功能的FW需要测试,要求测试工具遍历每个FW, 调用其他的程序,更新到磁盘固件后,做之前的多进程读写比较流程
重点关注如何调用其他程序。假设FW更新程序是FirwmareUpdateTool.exe,接受FW相关的参数
需要实现:

  • 界面接收参数
  • 调用其他程序,传参,且注意与主线程的同步

代码:

BOOL CmyMFCDlg::DoUpdateFirmware(CString filename)
{
	TCHAR szFilePath[MAX_PATH + 1] = { 0 };
	GetModuleFileName(NULL, szFilePath, MAX_PATH);
	(_tcsrchr(szFilePath, _T('\\')))[1] = 0;

	CString strToolPath(szFilePath);
	strToolPath = strToolPath + _T("FirwmareUpdateTool.exe");
	CString strPath;
	strPath.Format(_T("%s %s %s %d"), strToolPath.GetBuffer(0), m_str_VendorID.GetBuffer(0), filename.GetBuffer(0), m_SlotID);
	
	strAppend = strPath;
	ShowLogInEditBox();

	if (!PathFileExists(strToolPath))
	{
		strAppend.Format(_T("The %s is not exist!"), strToolPath.GetBuffer(0));
		ShowLogInEditBox();
		MessageBox(strAppend, MB_OK);
		return FALSE;
	}

	STARTUPINFO si = { sizeof(STARTUPINFO) };//在产生子进程时,子进程的窗口相关信息
	PROCESS_INFORMATION pi;                  //子进程的ID/线程相关信息
	memset(&pi, 0, sizeof(PROCESS_INFORMATION));
	DWORD returnCode = -1;              //用于保存子程进的返回值;

	BOOL bRet = CreateProcess(              //调用失败,返回0;调用成功返回非0;
		NULL,                               //一般都是空;(另一种批处理情况:此参数指定"cmd.exe",下一个命令行参数 "/c otherBatFile")
		strPath.GetBuffer(0),              //命令行参数         
		NULL,                               //_In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes,
		NULL,                               //_In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
		FALSE,                              //_In_        BOOL                  bInheritHandles,
		CREATE_NEW_CONSOLE,                 //新的进程使用新的窗口。
		NULL,                               //_In_opt_    LPVOID                lpEnvironment,
		NULL,                               //_In_opt_    LPCTSTR               lpCurrentDirectory,
		&si,                                //_In_        LPSTARTUPINFO         lpStartupInfo,
		&pi);                               //_Out_       LPPROCESS_INFORMATION lpProcessInformation

	if (bRet)
	{
		while (TRUE) //这里也是为了输出打印和日志而等待进程,同时也阻塞了主线程
		{
			DWORD result;
			MSG msg;
			result = MsgWaitForMultipleObjects(1, &pi.hProcess, FALSE, INFINITE, QS_ALLINPUT);
			if (result == (WAIT_OBJECT_0))
			{
				//获取子进程的返回值
				GetExitCodeProcess(pi.hProcess, &returnCode);
				CloseHandle(pi.hThread);
				CloseHandle(pi.hProcess);
				break;
			}
			else
			{
				PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
				DispatchMessage(&msg);
			}
		}
		strAppend.Format(_T("%s returnCode : %d "), strToolPath.GetBuffer(0), returnCode);
		ShowLogInEditBox();
	}
	else
	{
		strAppend.Format(_T("Start the %s failed!"), strToolPath.GetBuffer(0));
		ShowLogInEditBox();
		MessageBox(strAppend, MB_OK);
	}

	if (!returnCode)
	{
		return TRUE;
	}
	return FALSE;
}

CreateProcess创建进程,执行第三方程序
MsgWaitForMultipleObjects等待第三方进程返回,阻塞了当前主进程

小结

本文涉及的知识点:

  • 界面控件与底层类的数据交互
  • MFC的文件,字符串操作
  • 线程创建和线程同步
  • 线程通信:消息机制
  • 进程创建与同步