65.9K
CodeProject 正在变化。 阅读更多。
Home

在工作线程中监视和控制递归函数

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.26/5 (9投票s)

2008年8月31日

CPOL

12分钟阅读

viewsIcon

47704

downloadIcon

889

在这种情况下,如何很好地列出子文件夹中的文件。

修订版前言

你是否曾经回想起某件看似不久前发生的事情,但当你再次回想时,却发现它其实发生在很久很久以前?嗯,这正是我第一次尝试解决这个问题时隔了多久。至少有那么久。而且我后来又尝试过几次...

当然,我也在网上搜索过,看看是否有其他人写好的现成的解决方案。如果存在,我并没有找到。

2008年8月,我找到了一个解决方案,并认为把它发布在这里CodeProject上是个好主意。事实证明也确实如此——至少对我来说是这样,我希望对其他人也是如此。它对我很有帮助,因为引起了其他CodeProject成员的评论,这给了我动力回去重写代码,以及大部分文章,并希望这次能做得更好。

在这篇文章的早期版本中,我展示了一些代码(从现在开始我将称之为“朴素”代码),这些代码(据我测试)是有效的。然而,我当时并没有使用最优的多线程技术(因为文献对我来说有点难理解)。尽管如此,我对自己所做的工作感到自豪,并坚信这是网上唯一可以免费获取的、能实现此功能的MFC代码,所以我还是发布了它。

我希望我发布的并不最优的代码没有误导太多人对多线程编程的理解。如果确实如此,也许最新版本能有所弥补。

这篇文章的出现是我与自身无知持续斗争的体现。最新版本中的任何改进都直接得益于CodeProject社区成员提供的慷慨帮助和建议。本文中任何不尽如人意的地方完全是我自己的责任。

“朴素”代码的全部可耻之处可以在本文早期版本的CodeProject存档中找到。如果你点击本文标题“Revision:”旁边的“See All”链接,就可以找到它们。

引言

在本文中,我将描述一个应用程序,其中包含以下特定示例:

  1. “用户界面线程/工作线程”模型,其中:
    • 工作线程包含一个递归函数。
    • 存在一个通过UI线程控制和监视工作线程的机制。
  2. 使用“事件”同步线程。
  3. 线程安全问题。
  4. 通过“this”指针与工作线程通信。

我还会与我之前写的一些朴素代码进行比较,这些代码是在我学会如何正确地完成这项工作之前写的。我这样做是为了提供一些“如何不做”的示例。

该应用程序使用工作线程在提供信息流的同时遍历子文件夹列出文件,该信息流由UI以可控、用户友好的方式实时显示。

从现在开始,我将偶尔将该应用程序称为“进程”。当讨论线程和/或并行计算时,应用程序被称为“进程”。

为什么我将此任务分成两个线程?

    MFC期望你将任何耗时或处理器密集型活动放入称为“工作线程”的东西中。在此设置中的另一种线程称为“用户界面”或“UI”线程。从名称可以看出,这是你可以控制工作线程的线程。然而,UI线程最重要的是它被称为“消息泵”。

    如果你将耗时或处理器密集型活动放在UI线程中,你会发现当工作正在进行时,应用程序会变得无响应——无论你在循环中设置多少条件来检查是否点击了“取消”按钮等。我想说的是,消息泵应该只包含最少的额外处理,以便它能够实现其保持应用程序响应的目标。

什么是“线程安全”以及此应用程序如何符合要求?

    当进程中有两个或多个线程同时运行时(或在多个进程中),它们应该被设计成“线程安全”的。通过实现健全的线程同步技术来实现线程安全。这意味着会设置机制,防止各种线程和进程在不应访问或更改资源时访问或更改它们。

    这些资源共享机制通常列出为:

    • 临界区
    • 互斥锁
    • 信号量
    • 事件

    关键段、互斥锁和信号量都用于在线程内部一次性直接控制对单个资源的访问。事件则不同。使用事件,你可以让线程等待由其他线程或进程控制的一个或多个资源。

    在MFC中(因此也在此应用程序中),事件使用 CEvent 类实现。

    在我上面提到的朴素代码中,我设法使用布尔标志和“Sleep”函数来创建互斥和同步。现在,请将使用布尔标志和sleep函数来同步线程的任何诱惑从你脑海中抹去。如果你看下面的图,你会发现使用 CEvent 对象的方式和你使用布尔标志的方式有相似之处。但是,CEvent 对象比布尔标志更聪明。它被设计用于与各种等待函数(例如 WaitForSingleObject)一起使用,这些函数在看到它们正在等待的对象变为已信号状态时释放线程——这当然比 Sleep 函数具有重大优势,Sleep 函数没有任何在睡眠周期结束前提前释放线程的机制。

    CEvent 对象的已信号状态使用 SetReset 函数设置。除非你在实例化 CEvent 对象时明确将其设置为“手动”,否则它将以“自动”模式工作,这意味着当线程被释放时,事件会自动重置。在示例中,我将使用的事件设置为“手动”,以便它们的已信号状态在UI线程中保持。

    此图显示了自动事件与手动事件的已信号状态的比较。

    diag.jpg

    可能,我在朴素版本代码中最糟糕的错误是未能充分考虑到 CString 对象本身并非固有线程安全这一事实。我设法通过使用布尔标志和至少一个 Sleep 语句让我的UI线程和工作线程在 CString 信息之间进行双向通信。现在,如果存在良好的同步机制,CString 对象就可以安全使用。工作线程能够通过 WaitForSingleObject 语句与UI线程中的 CEvent 对象同步,从而安全地从UI线程接收 CString 信息。然而,反过来——让UI线程接收来自工作线程的 CString 信息——我使用了“SendMessageTimeout”,因为它不需要实例化新的 CEvent 对象,但却能令人满意地在不损害线程安全的情况下将UI线程同步到工作线程。

演示程序

我这里给出的例子是一个通过递归子目录搜索(并能列出)文件的线程。该应用程序是一个MFC单文档界面。

你选择一个文件夹并点击“Go”。如果该文件夹中有任何文件或其子文件夹中有文件,你就会看到文件名在屏幕上飞速闪过。然后,当所有文件都被找到后,应用程序会检测到线程已完成运行并重置自身。这是“监视”部分。另外,你会注意到当线程运行时,“Go”按钮的标题已更改为“Cancel”。你可能已经猜到按钮在这种状态下的作用。这是“控制”部分。

screenshot.jpg

Using the Code

要创建类似的应用程序——以下是方法:

创建一个单文档界面应用程序,其视图类继承自 CFormView。在窗体上,添加具有以下ID和相关变量的编辑框和按钮(所有字符串变量都是 CString,按钮变量是 CButton):

IDC_EDITFILELIST m_strFileList
IDC_EDITCURFOLDER m_strCurFolder
IDC_BSET m_buttonSet
IDC_BGO m_buttonGo
IDC_EDITSTART m_strStart
IDC_EDITNOFOLDERS m_strNoFolders
IDC_EDITNOFILES m_strNoFiles

此应用程序使用以下自定义事件...

  • CURDIREVENT
  • CURFILEEVENT
  • ENDLISTINGEVENT
  1. RecurseThreadDlg.cpp 中,按如下方式向消息映射添加三个相应的项:
  2. BEGIN_MESSAGE_MAP(CRecurseThreadDlg, CDialog)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()
        //}}AFX_MSG_MAP
        ON_MESSAGE(CURFILEEVENT,OnCURFILEEVENT)
        ON_MESSAGE(CURDIREVENT,OnCURDIREVENT)
        ON_MESSAGE(ENDLISTINGEVENT,OnENDLISTINGEVENT) 
    END_MESSAGE_MAP()
  3. RecurseThreadDlg.h 中,添加如下三行以给自定义事件赋值...
  4. // RecurseThreadDlg.h : header file
    //
    
    #pragma once
    #include "afxwin.h"
    
    
    #define CURDIREVENT (WM_APP + 1)
    #define CURFILEEVENT (WM_APP + 2)
    #define ENDLISTINGEVENT (WM_APP + 3)
  5. RecurseThreadDlg.h 中,按如下方式声明自定义事件处理函数:
  6. // Implementation
    protected:
        HICON m_hIcon;
    
        // Generated message map functions
        virtual BOOL OnInitDialog();
        afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
        afx_msg void OnPaint();
        afx_msg HCURSOR OnQueryDragIcon();
        //Insert custom event message handler declarations here
        afx_msg LRESULT OnCURDIREVENT(UINT wParam, LONG lParam);
        afx_msg LRESULT OnCURFILEEVENT(UINT wParam, LONG lParam);
        afx_msg LRESULT OnENDLISTINGEVENT(UINT wParam, LONG lParam);
    
        DECLARE_MESSAGE_MAP()
  7. 要添加到视图类中的其他成员变量...
  8. private:
        long m_iNoFiles;
        int m_iNoFolders;
  9. 我将这些事件处理程序添加到了 RecurseThreadDlg.cpp 中。
  10. LRESULT CRecurseThreadDlg::OnCURDIREVENT(UINT wParam, LONG lParam)
    {
        CString* pString = (CString*)wParam;
        CString tempStr = pString->GetBuffer();
        m_strCurFolder = tempStr;
    
        m_iNoFolders++;
        CString tString=LPCTSTR("");
        tString.Format(_T("%d"),m_iNoFolders);
        m_strNoFolders = tString;
        m_strFileList="";
    
        return 0;
    }
    
    LRESULT CRecurseThreadDlg::OnCURFILEEVENT(UINT wParam, LONG lParam)
    {
    
        CString* pString = (CString*)wParam;
        CString tempStr = pString->GetBuffer();
    
        m_iNoFiles++;
        CString tString=LPCTSTR("");
        tString.Format(_T("%d"),m_iNoFiles);
        m_strNoFiles = tString;
    
        m_strFileList= tempStr + _T("\r\n")+ m_strFileList;
        m_strFileList = m_strFileList.Left(200);
    
        UpdateData(FALSE);
        return 0;
    }
    
    LRESULT CRecurseThreadDlg::OnENDLISTINGEVENT(UINT wParam, LONG lParam)
    {
        m_buttonGo.SetWindowText(_T("Go"));
        m_strCurFolder=m_strNoFiles=m_strNoFolders=m_strFileList=_T("");
        m_buttonSet.EnableWindow(TRUE);
        return 0;
    }

    请注意,前两个处理程序如何通过它们的 wParam 参数接收 CString 数据。

  11. 一些初始化工作在对话框构造函数和 OnInitDialog 函数中进行。
  12. 您需要在 RecurseThreadDlg.h 中包含 afxmt.h 的头文件,以便使用 CEvent

    // RecurseThreadDlg.h : header file
    //
    
    #pragma once
    #include "afxwin.h"
    #include "afxmt.h"

    在下面的 OnInitDialog 函数中添加一行,以便在应用程序启动时禁用“Go”按钮。

    BOOL CRecurseThreadDlg::OnInitDialog()
    {
        CDialog::OnInitDialog();
    
        ...
    
        ...
    
        // TODO: Add extra initialization here
        m_buttonGo.EnableWindow(FALSE);
    
        return TRUE;  // return TRUE  unless you set the focus to a control
    }

    RecurseThreadDlg.h 中为以下事件添加声明(作为 public):

    CEvent* m_pEventStopped;       // Signaling indicates that thread
                                   // must stop.
    CEvent* m_pEventRootFolders;   // Gives the first thread the power to finish
                                   //    all other threads when the last folder it finds
                                   //    has been searched.
                                   // Taking over from BOOL m_bInitMakeListing;

    这些初始化在对话框类的构造函数中完成...

    CRecurseThreadDlg::CRecurseThreadDlg(CWnd* pParent /*=NULL*/)
        : CDialog(CRecurseThreadDlg::IDD, pParent)
        , m_strStart(_T(""))
        , m_strFileList(_T(""))
        , m_strCurFolder(_T(""))
        , m_strNoFolders(_T(""))
        , m_strNoFiles(_T(""))
        //next event is manual reset
        , m_pEventRootFolders(new CEvent(FALSE, TRUE))
        //next event is manual reset
        , m_pEventStopped(new CEvent(FALSE, TRUE))
    
    {
        //Set event to 'signalled' (not running)
        //m_PathEv.EventProceed = m_pEventStopped;
        m_pEventStopped->SetEvent();
        m_pEventRootFolders->SetEvent();
    
        m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
    }

    请注意,在 CEvent 构造函数的第二个参数中,传递了“TRUE”的值,这使得 CEvent 对象需要手动重置。这对于在我想重置事件之前保持事件状态是必要的。否则,事件会在线程释放时被重置。

    diag.jpg

    由于 CEvent 对象是在堆上创建的,因此需要专门销毁它们。可以在对话框类的析构函数中完成此操作。

    .h 文件中添加析构函数的声明。

    // Construction
    public:
        CRecThread2008_64Dlg(CWnd* pParent = NULL); // standard constructor
        ~CRecThread2008_64Dlg();                    // destructor

    ......并在 .cpp 文件中添加析构函数的定义......

    CRecThread2008_64Dlg::~CRecurseThread2008_64Dlg()
    {
        delete m_pEventRootFolders;
        delete m_pEventStopped;
    }
  13. 这些是按钮的事件处理程序:
  14. 请注意,“this”正在传递给工作线程。这是 RecurseThreadDlg 对象的地址,这意味着数据可以在线程和对象之间传输。

    void CRecurseThreadDlg::OnBnClickedBgo()
    {
        // TODO: Add your control notification handler code here
        m_iNoFolders = 0;
        m_iNoFiles = 0;
    
        if (::WaitForSingleObject(m_pEventStopped->m_hObject, 0)==WAIT_OBJECT_0){
            m_buttonGo.SetWindowText(_T("Cancel"));
            m_pEventRootFolders->ResetEvent();
            m_strCurFolder = m_strStart;
            m_pEventStopped->ResetEvent();
    
            AfxBeginThread(::RecursePathsGlobal,(void *) this,
                            THREAD_PRIORITY_LOWEST); 
    
            m_buttonSet.EnableWindow(FALSE);
        }
        else{
            m_buttonGo.SetWindowText(_T("Go"));
            m_pEventRootFolders->SetEvent();
            m_pEventStopped->SetEvent();
            m_buttonSet.EnableWindow(TRUE);
        }
        UpdateData(FALSE);
    
    }

    注意:我正在将一个下划线作为初始文件名传递给对话框。我发现,在此字段中传递一个值(任何值)都可以允许选择一个文件夹。

    void CRecurseThreadDlg::OnBnClickedBset()
    {
        // TODO: Add your control notification handler code here
        
        //This call worked OK with VS6 / Win XP
        //CFileDialog fD(TRUE,NULL,_T("_"),NULL,_T(""),NULL);
        //The following call worked better with VS2008 / Win 7 build
        CFileDialog fD(TRUE,NULL,_T("_"),NULL,_T("_"),NULL,0,FALSE);
    
        fD.DoModal();
    
        m_strCurFolder=m_strStart=m_strNoFiles=m_strNoFolders=m_strFileList=_T("");
        m_strCurFolder = fD.GetPathName();
        m_strCurFolder = m_strCurFolder.Left(m_strCurFolder.GetLength()-2);
        m_strStart.Format(m_strCurFolder);
    
        if (m_strStart != "")
            m_buttonGo.EnableWindow(TRUE);
        UpdateData(FALSE);
    
    }
  15. 这是工作线程函数:
  16. 如果希望工作线程成为类成员,则需要将此函数声明为“static”。

    注意...

    1. 对“SendMessageTimeout”的调用广播了用于监视线程中发生情况的自定义事件。其中两个使用该函数wParam参数通过文本信息进行发送。
    2. 对“m_pEventStopped”已信号状态的测试会导致线程响应线程外部接收到的用户输入。
    3. 参数 pParam 接收调用对象(RecThreadView 对象)的地址,并允许数据在线程和对象之间传输。
    UINT RecursePathsGlobal(LPVOID pParam)
    {
        //This is an adapted version of and very similar to the 
        //function given to the author by David Crow in correspondence
        //attached to and regarding the article "Monitoring and 
        //Controlling a Recursing Function in a Worker Thread" -
        //RecThread.aspx
        
        CFileFind fileFind;
    
        //Retrieving information from the calling object
        CRecurseThreadDlg * pMyView = (CRecurseThreadDlg *)pParam;
        CString * inString = &pMyView->m_strCurFolder;
        CString tString =  * inString + _T("\\*.*");
        LPTSTR Path = (LPTSTR)tString.GetBuffer(1);
        tString.ReleaseBuffer();
    
        // This code gives the View object information
        // it needs to enable/disable the 'set' button
        // and toggle the caption on the 'go' button
        // between 'go' and 'cancel'.
        BOOL FirstCall;
        if (WaitForSingleObject(pMyView->m_pEventRootFolders->m_hObject, 0) == 
                                                              WAIT_TIMEOUT)){
            FirstCall= TRUE;
            pMyView->m_pEventRootFolders->SetEvent();
        }
        else FirstCall = FALSE;
    
        BOOL bFound = fileFind.FindFile(Path);
        while (bFound)
        {
            // m_pEventStopped will stay in a signaled state so back out of this
            // recursive function by simply returning
            if (WaitForSingleObject(pMyView->m_pEventStopped->m_hObject, 0) == 
                                                              WAIT_OBJECT_0)
            return 0;
            else
            {
                bFound = fileFind.FindNextFile();
                if (fileFind.IsDirectory())
                {
                    if (! fileFind.IsDots())
                    {
    
                        CString * S = new CString(fileFind.GetFilePath());
                        SendMessageTimeout(pMyView->GetSafeHwnd(), 
                                           CURDIREVENT, (WPARAM)S, 0, 0,  0, 0);
                        delete S;
                        RecursePathsGlobal((LPVOID) pMyView);
                    }
                }
                else
                {
                    CString* pString = new CString(fileFind.GetFileName());
                    SendMessageTimeout(pMyView->GetSafeHwnd(), 
                                       CURFILEEVENT, (WPARAM)pString, 0, 0,  0, 0);
                    delete pString;
                }
            }
        }
        if (FirstCall == TRUE){
                pMyView->m_pEventStopped->SetEvent();
                SendMessageTimeout(pMyView->GetSafeHwnd(), 
                                   ENDLISTINGEVENT, 0, 0, 0,  0, 0);
        }
    }

就是这样。祝你递归愉快。不要把你的线程搞得一团糟。

灵感与信息

我感谢网上和MSDN库中许多文章的作者,我在撰写本文时参考了它们。

我特别感谢那些对我的文章以前版本以及我在CodeProject论坛上提出的问题做出回应的人,他们指出了潜在的缺陷,并给了我帮助和鼓励,包括:

  • S.H. Bouwhuis 是第一个评论原始文章的人,他促使我想改进它。他/她
    • 鼓励我阅读更多关于资源共享机制的内容。
    • 建议我使用线程安全的 CEvent 对象和 WaitForSingleObject 代替布尔标志在线程之间进行通信。
    • 指出工作线程函数可以是静态的或全局的。
  • David Crow 为提供了更优雅的工作线程启发式方法,我已将其放入我的代码中。谢谢!
  • David Delaune ('Randor') 提供:
    • 在确保同步正确性方面提供了具体帮助(用 WaitForSingleObject 替换 'Sleep')。
    • 识别出我的代码在没有我添加的“Sleep”调用时产生访问冲突错误的原因(即,我在线程之间直接访问了非线程安全的 CString)。
    • 建议我使用 SendMessageTimeout 函数向UI线程发送回消息。
  • Stephen Hewitt 鼓励我采用更具分析性的方法。
  • Chris Losinger,他的回答扩展了关于使工作线程成为静态还是全局的优缺点。

除了上述人员的帮助外,本文的创作还涉及大量参考MSDN库和搜索引擎工作。在推荐你查阅MSDN库以解决所有你不确定的问题的同时——你可能会发现以下内容值得一看……

历史

  • 2010-04-12:文章大修
    • 进行了广泛的代码重写以提高线程安全性。
    • 在文章中添加了讨论资源共享机制的内容。
    • 为最新修订版添加了新的前言。
    • 采用了更好的线程同步方法。
    • 解释了为什么应用程序采用多线程,引用了UI/工作线程模型。
    • 新版本代码使用VS2008而不是VS6编写。
    • 综合使用文本映射“_T("...")”处理字符串。
  • 2009-05-23:更新内容:
    • 添加了描述“this”指针用法的注释。
    • 修改了一些措辞以提高清晰度。
    • 包含对CodeGuru上Cilu关于“this”指针文章的引用。
  • 2008-08-31:首次发布。
© . All rights reserved.