问题
在《MFC环境下Start&Pause&Stop操作》的最后,提出了一个问题,即如果背景任务执行完了,该怎么办?
功能变更
为了说明问题,功能变更如下:
- Start:开始计算1到某个最大数(比如100)的平方根,并显示在UI上;
- 为了便于说明线程执行任务很耗时,在每次计算之后,Sleep一段时间。——现有代码已实现该功能。
- 在用户没有Pause/Stop的情况下,线程执行完所有计算任务之后,UI需要做适当的更新,类似于用户主动Stop一样,Start按钮要Enable、Stop按钮要Disable。
可能的方法
思路:
- 新增一个TaskFinished()函数:线程执行完计算任务之后,就调用这个函数。这个函数负责更新UI。
示例代码
头文件新增部分:
private:
DWORD m_dwMax;
void TaskFinished();
实现文件中的部分代码://为了便于调试,只计算到5的平方根;间隔时间为1s。
BOOL CStartPauseStopDlg::OnInitDialog()
{
...
m_dwMax = 5;
...
}
DWORD WINAPI CStartPauseStopDlg::ThreadProc(LPVOID lpThreadParameter)
{
CStartPauseStopDlg* pObj = (CStartPauseStopDlg*)lpThreadParameter;
HANDLE hSleepEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
HANDLE hEvents[2] = {hSleepEvent, pObj->m_hStopEvent};
for (;;) {
if (pObj->m_bStopped) {
//::AfxMessageBox("User stopped the task.");
break;
}
pObj->Doit();
if (pObj->m_dwCurrent == pObj->m_dwMax) {
pObj->TaskFinished();
break;
}
::WaitForMultipleObjects(2, hEvents, FALSE, 1000);
}
return 0;
}
void CStartPauseStopDlg::TaskFinished()
{
OnBnClickedStop();
}
到这里可能回想:为什么新增一个TaskFinished(),而不是直接在线程中调用OnBnClickedStop()?——此问题搁置不谈。
运行效果
运行后观察发现,当计算完之后,UI上的按钮的确自动更新了。不过,如果在这个时候,再试试Start、Stop的功能,会发现程序无响应了。
原因
线程调用了TaskFinished()函数,或者说OnBnClickedStop(),或者说CloseThread(),或者说WaitForSingleObject(m_hThread, INFINITE)。仅当这些调用完成之后,线程的代码才能继续往下跑。
另一方面,WaitForSingleObject(m_hThread, INFINITE)会一直等待线程结束,线程不结束,就一直Wait,直到INFINITE。
死锁,死了。
另一种方法
代码
看到了上面代码的问题,另一种可能的方法是:不管线程是否结束,只把界面更新一把即可:
void CStartPauseStopDlg::TaskFinished()
{
GetDlgItem(IDC_START)->EnableWindow(TRUE);
GetDlgItem(IDC_START)->SetWindowText(_T("Start"));
GetDlgItem(IDC_PAUSE)->EnableWindow(FALSE);
GetDlgItem(IDC_STOP)->EnableWindow(FALSE);
}
此时运行起来很好。
较真&无聊的想法(?)
线程调用TaskFinished()之后,UI看起来很好了,至少Start按钮Enable了,可以再Click了。
如果在用户Click这个Start按钮之前,线程还没有真正、干净地退出,会出现什么情况?如果线程干干净净地退出了,又会怎么样?
哦,反正这种代码发布出去,不知道什么时候会被抓去定位。。。。
又一种可能的方法
思路
- 线程把自己要完成任务的消息发出去(TaskFinished),然后自己退出。
- TaskFinished虽然不能直接WaitFor线程真正结束(否则两者循环依赖导致进程死掉),但它可以新发起一个线程,专门用于监测,如此不会阻塞TaskFinished()函数。
- 新增加的监测函数负责等待背景任务线程,当背景线程真正结束的时候,才去更新UI。
代码
头文件增加一个线程原型:
static DWORD WINAPI WaitForBgThreadExit(LPVOID lpThreadParameter);
实现文件:
BOOL CStartPauseStopDlg::OnInitDialog()
{
...
m_hStopEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
m_hThread = INVALID_HANDLE_VALUE;
m_bStopped = FALSE;
m_dwCurrent = 0;
m_dwMax = 5;
GetDlgItem(IDC_PAUSE)->EnableWindow(FALSE);
GetDlgItem(IDC_STOP)->EnableWindow(FALSE);
return TRUE; // return TRUE unless you set the focus to a control
}
void CStartPauseStopDlg::OnBnClickedPause()
{
GetDlgItem(IDC_START)->EnableWindow(TRUE);
GetDlgItem(IDC_START)->SetWindowText(_T("Continue"));
GetDlgItem(IDC_PAUSE)->EnableWindow(FALSE);
::SuspendThread(m_hThread);
}
void CStartPauseStopDlg::OnBnClickedStop()
{
m_bStopped = TRUE;
GetDlgItem(IDC_START)->EnableWindow(TRUE);
GetDlgItem(IDC_START)->SetWindowText(_T("Start"));
GetDlgItem(IDC_PAUSE)->EnableWindow(FALSE);
GetDlgItem(IDC_STOP)->EnableWindow(FALSE);
CloseThread();
m_dwCurrent = 0;
}
void CStartPauseStopDlg::OnDestroy()
{
CDialogEx::OnDestroy();
CloseThread();
::CloseHandle(m_hStopEvent);
}
DWORD WINAPI CStartPauseStopDlg::ThreadProc(LPVOID lpThreadParameter)
{
CStartPauseStopDlg* pObj = (CStartPauseStopDlg*)lpThreadParameter;
HANDLE hSleepEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
HANDLE hEvents[2] = {hSleepEvent, pObj->m_hStopEvent};
for (;;) {
if (pObj->m_bStopped) {
//::AfxMessageBox("User stopped the task.");
break;
}
pObj->Doit();
if (pObj->m_dwCurrent == pObj->m_dwMax) {
pObj->TaskFinished();
break;
}
::WaitForMultipleObjects(2, hEvents, FALSE, 1000);
}
return 0;
}
void CStartPauseStopDlg::Doit()
{
double d = sqrt(m_dwCurrent * 1.0);
m_sMsg.Empty();
m_sMsg.Format("sqrt(%d) = %lf", m_dwCurrent, d);
m_dwCurrent++;
//UpdateData(FALSE); //Debug Assertion Failed!
this->SetDlgItemText(IDC_MSG, m_sMsg);
//::AfxMessageBox("Do something ...");
}
void CStartPauseStopDlg::CloseThread()
{
if (INVALID_HANDLE_VALUE == m_hThread) return;
m_bStopped = TRUE;
::SetEvent(m_hStopEvent);
::WaitForSingleObject(m_hThread, INFINITE);
::CloseHandle(m_hThread);
m_hThread = INVALID_HANDLE_VALUE;
}
void CStartPauseStopDlg::TaskFinished()
{
HANDLE hThread = ::CreateThread(NULL, 0, WaitForBgThreadExit, this, 0, NULL);
::CloseHandle(hThread);
}
DWORD WINAPI CStartPauseStopDlg::WaitForBgThreadExit(LPVOID lpThreadParameter)
{
CStartPauseStopDlg* pObj = (CStartPauseStopDlg*)lpThreadParameter;
pObj->OnBnClickedStop();
return 0;
}
竞争无处不在
考虑这种场景:
- 线程执行完了任务,调用了pObj->TaskFinished();
- TaskFinished()发起的线程最终走到CloseThread()函数,但还在if之前,即线程句柄合法;
- UI上Stop仍处于Enable状态,用户此时单击了这个Stop按钮;
- 用户的这个stop操作,让流程也走到CloseThread()函数,而且也恰好走到if语句这里。
- 现在相当于有两个流程,都走到了CloseThread()这个函数。
此时,线程句柄合法,两个流程继续往下,比如:
- 第一个流程顺利等待背景线程结束、关闭了线程句柄(CloseHandle)、重置线程句柄为INVALID_HANDLE_VALUE;然后第二个流程获取了CPU控制器,继续往下走;
- 当走到WaitFor的时候,此时句柄为INVALID_HANDLE_VALUE。MSDN的WaitFor并没有特别地提到这种情况如何处理。不过,通过一个小程序发现,此时系统任务该句柄INVALID_HANDLE_VALUE无信号,于是一直等待。如果超时时间为INFINITE,就永远等下去。
注:线程句柄在CloseHandle之后,再传给WaitFor,其返回-1(WAIT_FAILED),提示“句柄无效”。同样传给WaitFor一个NULL句柄,也是返回-1,提示“句柄无效”。但传入一个INVALID_HANDLE_VALUE则被认为是合法的句柄。
因此,需要对这种可能的异常做好准备。简单的一种方法,就是把CloseThread()中的处理流程作为一个Critical Section。即代码可以优化为:
void CStartPauseStopDlg::CloseThread()
{
m_criticalSecion.Lock();
if (INVALID_HANDLE_VALUE != m_hThread) {
m_bStopped = TRUE;
::SetEvent(m_hStopEvent);
::WaitForSingleObject(m_hThread, INFINITE);
::CloseHandle(m_hThread);
m_hThread = INVALID_HANDLE_VALUE;
}
m_criticalSecion.Unlock();
}
这里新增了一个数据成员:
CCriticalSection m_criticalSecion;