MFC环境下Start&Pause&Stop操作(续):我执行完了

23 篇文章 0 订阅

问题

在《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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值