当前位置:C++技术网 > 资讯 > 使用多线程避免窗口卡死(假死)的实现方案

使用多线程避免窗口卡死(假死)的实现方案

更新时间:2017-07-24 13:11:17浏览次数:1+次

    本文是对窗口卡死(假死)的应用问题进行一系列的探讨,旨在帮助对Windows应用程序不熟悉的新手理解这个问题。我将逐步的深入开发的过程,让你有一个循序渐进的思路。
    我这里就用MFC程序作为示例,当然win32程序或者C#等都可以实现,这里主要是做一个参考。
    一般来说,我们点击了一个按钮,就触发了一个动作,这个动作就可以执行一系列的功能。但是有的功能可能会耗时很久,比如下载文件,或者读写文件等。此时窗口界面就无法再响应我们的操作了,然后就出现了窗口提示“未响应”。实际上窗口程序在执行功能,并没有死掉,所以也叫作假死。我们一般也将这样的情况叫做卡死。发生未响应的假死状态,是因为当一个窗口事件执行的太久的时候,你后续的操作来不及处理,所以就无法响应了。这样的现象会导致体验很差。
    但是多数时候,我们都只是执行一个简单的动作,3秒内都可以完成,很多时候也就懒得进行处理了,耐心一点就是了。但是如果超过了3秒,很多用户就不耐烦了,再操作界面,程序就未响应,然后用户以为程序死掉了,然后就强制关闭了窗口程序,然后就会骂软件做的很烂。如果确实只没有时间去改善,怎么办呢?要么在界面上显示一行文字说明,请等待。但是事先显示在界面上不美观,不显示用户不知道。怎么办呢?而当窗口在执行一系列操作的时候,无法更新界面了,所以设置界面上的控件也是无法更新提示文字了。怎么办呢?
    下面就这个问题的界面,按照解决问题的思路,一个个给出解决办法。首先是让卡死(假死)的窗口能够更新提示,最好是能够动态的变化的提示。然后最好是不要卡死。如果不卡死,也就是说,尽管要执行的操作还没有完成,也要让界面可以自由的操作。不过这个会引起一个问题,那就是可能会触发更多的误操作,比如又执行了一次功能,或者前面功能没有执行完就点击了后续的功能的执行,会导致程序逻辑的混乱。所以,我们还要对改进了界面体验之后出现的新问题再进行处理。
    当然,我们会一步步的将解决的办法告诉你的。开始吧!
1.让卡死的界面依然可以动态更新提示
    当我们的按钮单击之后,在执行功能,需要耗时10S。在这10S内,我们是无法操作界面了。如果操作,可能就让程序陷入“未响应”的状态。而我们要向改善这个体验,我们可以在进入执行功能时,在界面上动态提示执行的过程,让用户等待,提示执行的进度。
    默认情况下,我们都是在按钮的命令处理编写功能代码。这样实现很简单。对于大多数不耗时的功能就够了。我们先来写一个模拟耗时10S的功能代码,代码如下:
void Ct1Dlg::OnBnClickedOk()
{
    for (int i=0;i<10;i++)
    {
        Sleep(1000);
    }
}
    当你点击按钮的时候,就会睡眠10秒,表示内部执行了10秒的时间长度。此时界面就无法响应用户的操作了。然而此时却在界面看不到任何的提示。所以我们要想办法将动态的提示显示到界面上。前面分析说了,控件是没有办法更新内容的,因为此时正在执行按钮消息WM_COMMAND。这个消息处理不执行完,不会去执行其他消息的处理的。因为窗口界面消息的处理是在一个线程中的,默认的就是主线程。主线程在执行内部的功能,所以界面就会卡死。当主线程把内部功能执行完了,就会继续处理其他的消息,你就看到你操作的界面有反应了。
    那么如何将动态提示更新到窗口上呢?我们可以更新窗口的标题文字。代码如下:
void Ct1Dlg::OnBnClickedOk()
{
    for (int i=0;i<10;i++)
    {
        Sleep(1000);
        str.Format(_T("已耗时%d秒,请稍候..."),i+1);
        SetWindowText(str);
    }
}
    此时界面如下所示:
    
    虽然界面卡住不能操作了,但是在标题上却可以动态的更新功能执行的进度,这样用户就知道程序还在执行,并没有死掉,也就不会去乱点了。

2.虽然可以提示内部功能执行的进度,但是如果能够操作界面就更好了。
    我们前面说了,窗口操作是主线程在执行的,如果主线程陷入了一个功能执行的流程中,要花一段时间,那么在这个时间里是没法响应其他界面操作的,所以卡住了。那么我们如果再开一个线程,让这个线程去执行长时间的功能代码,界面这个主线程不就可以随时处理界面操作了嘛。所以,我们创建一个线程就可以解决卡死的问题。创建线程用CreateThread,这个函数的具体使用,请查阅MSDN。虽然参数很多,但是大多数参数都是可以设置为0的。我们只需要传入两个参数就可以了,一个就是线程函数,一个是接收创建后线程的ID的。创建线程的代码如下:
DWORD id;
HANDLE hTread = CreateThread(0,0,(LPTHREAD_START_ROUTINE)&Ct1Dlg::ShowCount,0,0,&id);
CloseHandle(hTread);
    如果我们的处理线程是全局的函数,就没有“&Ct1Dlg::”这个东西。这个是指定类中的静态函数的。而LPTHREAD_START_ROUTINE则是CreateThread函数要求的参数类型,我们强制转换一下就行了。线程函数就是一个没有传入参数的,返回值为void的函数。变量id则用于接收创建好的线程的线程ID,其他参数全部设置为0即可。返回的值为线程的句柄,在随后的一句代码中,我们关闭句柄,避免资源泄露。更多相关内容,请阅读《Windows核心编程 》的第二部分第五章。
    我们提供的线程函数就是这样的:
void Ct1Dlg::ShowCount()
{
    CString str;
    HWND hwnd = AfxGetApp()->GetMainWnd()->GetSafeHwnd();
    for (int i=0;i<10;i++)
    {
        Sleep(1000);
        str.Format(_T("已耗时%d秒,请稍候..."),i+1);
        ::SetWindowText(hwnd,str);
    }
}
    在线程函数中,代码稍微有点差别。因为这个函数必须是静态函数。静态函数就和全局函数(C语言函数)一样的,不能直接操作对象的成员变量。所以在操作窗口的时候,只能通过窗口句柄来操作。使用的函数也得是Windows的API函数。所以有些函数前面有::,就是表示是API函数,不是MFC的窗口类函数。而我们要获取主窗口的句柄,也就是函数中第二句执行的代码的作用,依次是获取APP对象的指针,然后获取主窗口的指针,再获取主窗口的句柄。后续就可以使用::SetWindowText来设置主窗口的文字了。
    到这里,我们就可以在点击按钮后,让新创建的线程执行长达10秒钟的功能,而界面依然可以自由操作了。
3.然而问题又来了,在这10秒钟内,我们再次点击按钮,又创建了一个线程执行同样的操作,或者点击了后续步骤的按钮,就会导致逻辑混乱了。
    我们先就以点击的这个按钮为例说明一下解决办法。我们最直接能想到的就是,在10秒内,我们将界面不能操作的按钮或其他控件禁用,等后台线程执行完毕之后,来恢复按钮。这样就可以控制好界面的按钮的合理使用了。
    对于按钮的操作,我们依然还是得通过主窗口的句柄来获取控件的句柄,然后再执行操作。
    完整的代码如下:
//声明:
public:
    afx_msg void OnBnClickedOk();
    static void WINAPI ShowCount();
//实现:
void Ct1Dlg::OnBnClickedOk()
{
    ShowCount();
    DWORD id;
    HANDLE hTread = CreateThread(0,0,(LPTHREAD_START_ROUTINE)&Ct1Dlg::ShowCount,0,0,&id);
    CloseHandle(hTread);
    GetDlgItem(IDOK)->EnableWindow(FALSE);
}
void Ct1Dlg::ShowCount()
{
    CString str;
    HWND hwnd = AfxGetApp()->GetMainWnd()->GetSafeHwnd();
    for (int i=0;i<10;i++)
    {
        Sleep(1000);
        str.Format(_T("已耗时%d秒,请稍候..."),i+1);
        ::SetWindowText(hwnd,str);
    }
    HWND hBtn = ::GetDlgItem(hwnd,IDOK);
    ::EnableWindow(hBtn,TRUE);
}
    下面是效果图:
1.启动了后台功能执行,并使控件禁用了
1.启动了后台功能执行,并使控件禁用了
2.执行后台功能时移动了界面
2.执行后台功能时移动了界面
3.后台功能执行完后恢复了控件的使用
3.后台功能执行完后恢复了控件的使用