作者:陈飞
一、前言
文件读取进度条的实现可以有很多种方法,常用的是在读取文件的过程中隔一定时间向对话框发送消息以控制进度条的位置,但是这种方法很难确定隔多少时问发送一个消息,因为文件的大小是不确定的,时间间隔长了可能文件已经读取完了还没有发送消息,而消息发送得太频繁又会影响文件读取的效率。特别是在读取文本文件时你可能需要在每一个ReadString()函数之后都要发送一个消息,而在一些格式比较复杂的文件读写代码中(例如dxf文件的读取),这样的读取函数循环可能有几十处,在这样的代码中发送消息是很繁琐的事情。而利用线程同步则可以很好地解决这个问题。
进程是一个可执行的程序,由私有虚拟地址空间、代码、数据和其他操作系统资源(如进程创建的文件、管道、同步对象等)组成。一个应用程序可以有一个或多个进程,一个进程可以有一个或多个线程,其中一个是主线程。
线程是操作系统分时调度分配CPU时问的基本实体。一个线程可以执行程序的任意部分的代码,即使这部分代码被另一个线程并发地执行,一个进程的所有线程共享它的虚拟地址空间、全局变量和操作系统资源。
创建一个新的进程必须加载代码,而线程要执行的代码已经被映射到进程的地址空间,所以创建、执行线程的速度比进程更快。另外,一个进程的所有线程共享进程的地址空间和全局变量简化了线程之间的通信,所以以线程为调度对象要比以进程为调度对象效率高。但是在几个线程并行运行时,可能会存在线程的同步问题。例如:两个线程同时对一个全局数组进行操作,线程A取得对该数组的控制权对数组进行写入,当写入还未完成时,控制权又由线程B取得,线程B改变了该数组的数据,然后线程A又取得控制权进行读取,这样线程A获取的数据可能并不足其所需要的数据,这时就要用线程同步来解决这个问题。
Windows提供了几种同步对象来实现线程的同步,常用的有临界区(critical section)、互斥量(mutexe)、信号量(semaphore)、事件(event)和可等待的记时器(waitable timer)等。线程主要使用两个函数将它们设为睡眠来等待内核对象变为有信号:
DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut) DWORD WaitForMultipleOblects(DWORD cObject, LPHANDLE lpHandles)
函数WaitForSingleObject告诉系统线程在等待由参数hObject标识的内核对象变为有信号。参数dwTimeOut告诉系统线程愿意等待多少毫秒。如果指定的内核对象在指定时间内没有变为有信号,系统就会唤醒线程,让它继续执行。函数的返回值有3种:WAIT_OBJECT_0表示对象达到有信号状态;WAIT_TIMEOUT表示对象在dwTimeOut毫秒内未达到有信号状态;WAIT_ABANDONED表示对象是一个互斥量,由于被放弃而达到了有信号的状态。
二、实现方法
首先我们声明一个全局的文件指针g_pFile以存放要读取的文件指针,然后在主线程中指定要读取的文件,初始化g_pFile,并进行进度条对话框的创建。然后打开两个线程ReadDxfThreadProc(LPVOID lParam)和SetProgressPosThreadProc(LPVOID lParam),第一个线程用来读取文件,第二个线程则通过g_pFile指针来判断当前文件指针的位置,并向对话框发送消息以控制进度条的位置。这样我们将文件读取和向对话框发送消息分离,就不必在每个文件读取语句后都发送消息了,而且文件读取的效率也较高:
在编码过程中有以下几个问题要注意:
(l)在创建两个线程的时候必须将它们的优先级设置为同级,否则会造成一个线程已经结束了而另一个线程还没有开始。
(2)由于两个线程都要用到g_pFile指针,因此需要对两个线程进行同步,否则会出现异常。在本实例中我们利用互斥量来对两个线程进行同步。在声明全局文件指针的同时我们也声明一个互斥量句柄:HANDLE hMutex = NULL,并在主进程打开线程之前创建互斥量,对g_hMutex进行初始化:g_hMutex = CreateMutex(NULL, FALSE, NUlL);
(3)由于本实例中进度条对话框使用的是非模态划话框,因此除了需要定义设置进度条位置的消息响应函数之外,还需要定义销毁对话框的消息响应函数。
(4)在线程SetProgressPosThreadProc中我们通过全局变量g_pFile来探测文件读写的进度。当文件读写完毕以后我们需要对非模态的进度条对话框发送一个销毁对话框窗的消息以关闭对话框。
(5)当文件读取完毕以后,我们需要将全局的文件指针g_pFile销毁并置空。由于在SetProgressPosThreadProc线程中我们循环的条件是文件当前位置小于文件的长度,所以当循环跳出时就说明读取文件的线程已经执行完毕,这时我们就可以关闭g_pFile指针了。如果将指针的关闭放在ReadDxfThreadProc线程中执行,则可能出现文件指针已关闭,而SetProgressPosThreadProc线程中仍在调用文件指针的情况。
三、程序实现
(1)利用AppWizard创建一个MFC AppWizard(EXE)的单文档工程,取名为:ReadFile。(2)新建一个对话框,并为该对话框创建一个类CProgressDlg。
(3)进度条控件和百分数的静态文本框定义变量:
CProgressCtrl m_ctrlProgress; CStrina m_szPercent;
(4)为对话框添加函数BOOL Create(),以创建非模态对话框。函数实现如下:
BOOL CProgressDlg::Create() { return CDialog::Create(CProgressDlg::IDD, NULL); }
(5)在对话框初始化时要设置进度条的范围:
BOOL CProgressDlg::OnInitDialog() { CDialog::OnInitDialog(); m_ctrlProgress.SetRange(l0, 100); return TRUE; }
(6)由于线程是通过向对话框发送消息来控制进度条的位置和销毁对话框,所以我们必须在ProgressDlg.h文件中定义两个自定义消息:
#ifndef WM_UPDATE_DXF_DLG_POS //更新进度条位置的消息 #define WM_UPDATE_DXF_DLG_POS WM_USER+10000; #endif #ifndef WM_UPDATE_DXF_DLG_DESTROY //销毁对话框的消息 #define WM_UPDATE_DXF_DLG_DESTROY WMUSER+10001 #endif
(7)建立消息响应函数。
在ProgressDlg.cpp文件中END_MESSAGE_MAP()之前加入以下代码: ON_MESSAGE(WM_UPDATE_DXF_DLG_POS, OnUpdateProgressPos) //更新进度条位置的消息响应函数 ON_MESSAGE(WM_UPDATE_DXF_DLG_DESTROY, OnDestroyDlg) //销毁对话框的消息响应函数
然后在ProgressDlg.h文件中DECLARE_MESSAGE_MAP()之前加入以下代码:
afx_msg LRESULT OnUpdateProgressPos(WPARAM wp, LPARAM lp); afx_msg LRESULT OnDestroyDlg(WPARAM wp, LPARAM lp);
在ProgressDlg.cpp文件中添加消息响应函数实体:
LRESULT CProgressDlg::OnUpdateProgressPos(WPARAM wp, LPARAM lp) //传入的参数wp即是计算过后当前进度争的位置 { m_ctrIProgress.SetPos((int)wp); //设置进度条位置 m_szPercent.Forrnat("%d", (int)wp); m_szPercent += "%"; UpdateData( FALSE); //设置百分数显示的静态更本 return 0; } LRESULT CProgressDlg::OnDestroyDlg(WPARAM wp, LPARAMp lp) { DestroyWindow(); //销毁对话框 return 0; }
(8)建立“确定”按钮响应函数。
由于我们采用的是非模态对话框,所以在此不能调用CDialog::OnOK()函数,也不能调用DestroyWindow()函数销毁对话框,因为此时可能SetProgressPosThreadProc进程还没有结束,还在向对话框发送消息,所以在此只是隐藏对话框,等到进程结束以后再发送消息销毁对话框。函数实体如下:
void CProgressDlg::OnButtonClose() { ShowWindow(SW_HIDE); }
(9)在ReadFileView.cpp文件中定义全局变量和线程函数。代码如下:
CStdioFile* g_pFile = NULL; //全局的正件指针 HANDLE g_hMutex = NULL; //互斥量 DWORD WINAPI SelProqressPosTnreadProc(LPVOID lParam) //发送进度各位置消息的进程 { CDialog* pDlg =(CDialog*)lParam; //传入的进度条对话框指针 if(!pDlg) return 0; //异常判断 DWORD dPos = 0; //当前文件指针的位置 DWORD dLengtn = 1; //文件的长度 DWORD dProssPos = 0; //计算出来的进度条的位置 DWORD dPrePos = 0; //记录前一个进度条的位置 DWORD dw; dw = WaitForSingleObject(g_hMutex, INFINITE); //等待互斥量有信号 if(dw == WAIT_OBJECT_O); //判断互斥量是否为有信号 { dLength = g_pFile->GetLength(); //获取文件长度 } else //废弃的信号,说明读取文件的线程有异常 { ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, 100, NULL); // ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_DESTROY, NULL, NULL); //向对话框发送消息,以销毁对话框 return O; } ReleaseMutex(g_nMutex); //释放互斥量 while(dPos < dLength) //当文件未读完时执行循环 { if(!pDlg) return 0; //异常判断 dw = WaitForSingleObject(g_nMutex, INFINITE); //等待互斥量有信号 if{dw == WAIT_OBJECT_O) //等待到信号 { dPos = g_pFile->GetPosition(); //获取当前文件指针的位置 dProssPos = DWORD(double(dPos)/dLength * 100); //计算进度条的位置 } else //废弃的信号 { ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, 100, NULL); ::SendMessaqe(pDlg->GeiSafeHwnd(), WM_UPDATE_DXF_DLG_DESTROY, NULL, NULL); //向对话框发送消息,以销毁对话框 return 0; } ReleaseMutex(g_hMutex); //释放互斥量 if(dProssPos != dPrePos) //进度条位置相同时不发送更新消息 { ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, dProssPos, NULL); } dPrePos = dProssPos; //当前位置变为前一位置 } //由于计算位置时用的将double强制转换为DWORD型,有可能没有计算到100 //所以需要发送一个消息将进度条位置刷新到100 ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_POS, 100, NULL); Sleep(500); ::SendMessage(pDlg->GetSafeHwnd(), WM_UPDATE_DXF_DLG_DESTROY, NULL, NULL); //向对话框发送消息,以销毁对话框 //文件读取完毕以后要对文件指针进行关闭和销毁 dw = WaitForSingleObject(g_hMutex, INFINITE); if(dw == WAIT_OBJECT_0) { g_pFile->Close(); delete g_pFile; g_pFile = NULL; } else //废弃的信号 { g_pFile->Close(); delete g_pFile; g_pFile = NULL; } ReleaseMutex(g_hMutex); return 0; } DWORD WINAPI ReadDxfThreadProc(LPVOID lParam) //读取文件的线程 { CString strTernp = ""; //存放读取的字符 BOOL bisEnd = TRUE; //指示文件是否读取完毕 DWORD dw,//互斥量信争结襄 while(1) { if(!g_pFile) return 0; //异常判断 dw = WaitForSingleObject(g_hMutex,INFINITE); if(dw == WAIT_OBJECT_0) //互斥量等待到信号 { blsEnd = g_pFile->ReadString(strTemp); //读取文件 } else if(dw == WAIT_ABANDONED) return 0; ReleaseMUtex( g_hMutex): if(blsEnd == FALSE) break; } return 0; }
(10)在CReadFileView中添加进度条对话框对象成员。这个对象不能在调用时声明,因为主线程执行完后就会销毁对象,这样在线程中调用对话框指针就会出现异常。
在ReadFileView.h文件中添加:CProgressDlg m_ProgressDlg;
(11)在主框架菜单中添加下拉菜单“测试”,在“测试”菜单下添加子菜单“读取dxf文件”,然后为子菜单添加响应函数OnReadDxfFile(),函数代码如下:
void CReadFileView::0nReadDxfFile() { if(m_ProgressDlg.GetSafeHwndl)) //如果对话框没有销毁,说明上一个更件读取线程还没有结束,因此要返回 { MessageBox("文件还未读取完毕,请稍后!", NULL, MB_0K); return; } CString strName == ""; //举买例采用dxf五件为读取的文本文件类型 CFileDialog dlg(FALSE, "dxf", "$dxf", OFN_HIDEREADONLY, "dxf file(*.dxf) | *.dxf||", NULL); if(dlg.DoModal() == IDOK) { strFileNarne = CString(dlg.GetFileName()); g_pFile = new CStdioFile(strFileName, CFile::modeRead); //初始化文件指针 m_ProgressDlg.Create(); //开始创建非模态对话框 g_hMutex = CreateMutex(NULL, FALSE, NULL); //创建互斥量 //创建读取文件的线程和发送进度条位置消息的进程 //创建SetProgressPosThreadProc线程时将非模态对活框的指针传入 HANDLE hThreads[2]; DWORD dwThreadID; //存放线程ID hThreads[0] = CreateThread(NULL, 0, SetProaressPosThreadProc, &m_ProgressDlg, O, &dwThreadID); hThreaos[1] = CreateThread(NULL, 0, ReadDxfThreadProc, NULL, 0, &dwThreadID); CloseHandle(hThreads[0]); CloseHandle(hThreads[1]); }
(12)运行结果如图1-61所示。