Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / HPC / parallel-processing

Monitoring and Controlling a Recursing Function in a Worker Thread

4.26/5 (10 votes)
13 Apr 2010CPOL12 min read 1   835  
In this case, how to list files in sub folders nicely.

Foreword to the revised version

Have you ever thought about something that happened seemingly not all that long ago and then you think about it again, and actually it happened ages and ages ago? Well, that was how long ago I first tried to solve this problem. At least that long ago. And I've tried to solve it on several occasions since...

Of course, I searched the web to see if there was a convenient ready made solution available written by someone else. If there was one I didn't find it.

In August 2008, I reached a solution and thought it would be a good thing to do to publish it here on CodeProject. And so it has proved to be - for me at least - and I hope for other people. It has been useful to me because of the comments that it provoked from other CodeProject members, which have given me the impetus to go back and rewrite the code and, also most of the article and, hopefully, this time to get it right.

In the earlier versions of this article, I presented some code (from henceforth I will refer to this as the 'naive' code) that (as far as my testing went) worked. However, I managed to avoid using optimal multi-threading techniques (due to the literature being somewhat hard for me to digest). Nevertheless, I was proud of what I'd done, and convinced it was the only MFC code that did what it did that was freely available on the web, so I went ahead and published it anyway.

I hope I haven't corrupted too many people's idea of multithreaded programming by publishing less than optimal code. If so, perhaps this latest version will go some way to make amends.

This article came into existence as a manifestation of the on-going struggle I have with my own ignorance. Any improvements in the latest edition are all directly as a result of the generous help and advice given to me by members of the CodeProject community. Anything that is less than wonderful in this article is entirely my own work.

The 'naive' code can be seen in all of its ignominy in the CodeProject archives of the earlier versions of this article. You will be able to find these if you click on the 'See All' link beside the 'Revision:' item in the header of this page.

Introduction

In this article, I describe an application which contains specific examples of the following:

  1. the 'user interface thread / worker thread' model where
    • the worker thread consists of a recursing function
    • there is a mechanism for controlling and monitoring the worker thread via the UI thread
  2. synchronizing threads using 'Events'
  3. thread safety issues
  4. communicating with the worker thread via the 'this' pointer

I will also make make comparisons with some naive code I produced before I learned how to do this job properly. I do this to provide a source of 'how not to do it' examples.

The application uses the worker thread to list files in subfolders whilst providing a stream of information which is displayed in real time by the UI in a controllable, user friendly fashion.

From this point onwards, I will occasionally refer to the application as the 'process'. When threads and/or parallel computing is being discussed, applications are referred to as 'processes'.

Why have I split this task into two threads?

    MFC expects you to put any long-winded or processor intensive activities in to what's called a 'worker thread'. The other kind of thread in this set-up is called the 'user interface' or 'UI' thread. By its name, you can tell that this is the thread by which you can control your worker thread. The most important thing about the UI thread, though, is that it is what's called a 'message pump'.

    If you put your long-winded or processor intensive activity in the UI thread, you will find that the application becomes unresponsive when work is being done - however many conditionals you may put in the loop for testing to see if the 'Cancel' button has been clicked, etc. I suppose the point is that the message pump should be free of anything but the minimum of extra processing in order that it can achieve its goal of keeping the application responsive.

What is 'Thread Safety' and how does this application comply?

    When there are two or more threads running together in a process (or among several processes), they should be designed to be 'thread safe'. Thread safety is achieved by implementing sound thread synchronization techniques. This means that mechanisms are put in place which prevent the various threads and processes from accessing or changing resources when they are not meant to.

    Those resource sharing mechanisms are commonly listed as:-

    • Critical Sections
    • Mutexes
    • Semaphores
    • Events

    Critical Sections, Mutexes, and Semaphores are all about directly controlling access to a single resource at a time within a thread. Events are different. With Events, you can make your thread wait for one or more resources that are controlled by other threads or processes.

    In MFC (and so in this application), Events are implemented using the CEvent class.

    I had managed, in the naive code I mentioned above, to create mutual exclusion and synchronization using boolean flags and a 'Sleep' function. Now, cast all temptation to use boolean flags and sleep functions to synchronize your threads from your mind. If you look at the diagram below, you can see that there are similarities in the way you would use a CEvent object and the way you would use a boolean flag. However, a CEvent object is cleverer than a boolean flag. It is designed to be used with one of a variety of wait functions (e.g. WaitForSingleObject) that release a thread at the moment when they see that the object(s) they are waiting for become(s) signaled - which, of course, is a major advantage over the Sleep function which has no facility for releasing a thread earlier than its sleep period.

    The signaled state of a CEvent object is set using the the Set and Reset functions. Unless you specifically make your CEvent object 'manual' when you instantiate it, it will work in 'automatic' mode, which means that the event is reset automatically when the thread is released. In the example, I have set the events I use to 'manual' so that their signaled state persists in the UI thread.

    This diagram shows the signaled state of an automatic event compared to a manual event:

    diag.jpg

    Probably, the worst mistake I made in the naive version of the code was to not properly cater for the fact that CString objects are not inherently thread safe. I had managed to get my UI and worker threads to communicate CString information back and forth from each other by using boolean flags and at least one Sleep statement. Now, CString objects can be used safely if good synchronization mechanisms are in place. The worker thread is able to safely receive CString information from the UI thread by synchronizing itself with the CEvent objects in the UI thread via WaitForSingleObject statements. To do this in reverse, however, - to allow the UI thread to receive CString information from the worker thread, I have used 'SendMessageTimeout' since it doesn't require a new CEvent object to be instantiated, yet satisfactorily synchronizes the UI thread to the worker thread in without compromising thread safety.

Demo Program

The example I give here is of a thread that searches (and can list) files through recursive subdirectories. The application is an MFC Single Document Interface.

You select a folder and click on 'Go'. If the folder has any number of files in it or in subfolders, you see their names whizz past on the screen. Then, when all of the files have been found, the application detects that the thread has completed running and resets itself. That is the 'monitoring' part. Also, you will notice that when the thread is running, the caption on the 'Go' button has changed to 'Cancel'. You will have guessed what the button does in that state. That is the 'controlling' part.

screenshot.jpg

Using the Code

To create a similar application - here's how to do it...

Create a Single Document Interface application with the view class inherited from CFormView. On the form, add edit boxes and buttons with the following IDs and associated variables (all string variables are CString and the button variables are CButton):

IDC_EDITFILELISTm_strFileList
IDC_EDITCURFOLDERm_strCurFolder
IDC_BSETm_buttonSet
IDC_BGOm_buttonGo
IDC_EDITSTARTm_strStart
IDC_EDITNOFOLDERSm_strNoFolders
IDC_EDITNOFILESm_strNoFiles

This application uses the following custom events...

  • CURDIREVENT
  • CURFILEEVENT
  • ENDLISTINGEVENT
  1. In RecurseThreadDlg.cpp, add three corresponding items to the message map, as follows...
  2. C++
    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. In RecurseThreadDlg.h, add the three lines as shown below to assign values to the custom events...
  4. C++
    // 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. In RecurseThreadDlg.h, declare the custom event handler functions as follows...
  6. C++
    // 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. Other member variables to be added to the View class...
  8. C++
    private:
        long m_iNoFiles;
        int m_iNoFolders;
  9. I added these event handlers to RecurseThreadDlg.cpp:
  10. C++
    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;
    }

    Note how the first two handlers receive CString data via their wParam parameters.

  11. Some initialization takes place in the dialog constructor and in the OnInitDialog function...
  12. You need to add an include for afxmt.h to RecurseThreadDlg.h so you can use CEvent.

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

    Add a single line to the OnInitDialog function below so that the 'Go' button is disabled when the application starts up.

    C++
    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
    }

    Add declarations for the following events in RecurseThreadDlg.h (as public):

    C++
    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;

    These initializations are in the constructor for the dialog class...

    C++
    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);
    }

    Note that in the second argument of the CEvent constructors, a value of 'TRUE' is passed to cause the CEvent objects to require a manual reset. This is necessary to maintain the state of the event until the point where I want it to be reset. Otherwise, the event is reset when the thread is released.

    diag.jpg

    Because the CEvent objects were created on the heap, they need to be specifically destroyed. You can do this in a destructor for the dialog class.

    Add a declaration for the destructor to the .h file.

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

    ...and a definition for the destructor to the .cpp file...

    C++
    CRecThread2008_64Dlg::~CRecurseThread2008_64Dlg()
    {
        delete m_pEventRootFolders;
        delete m_pEventStopped;
    }
  13. These are the event handlers for the buttons...
  14. Note that 'this' is being passed to the worker thread. This is the address of the RecurseThreadDlg object, and it means that data can be transferred between the thread and the object.

    C++
    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);
    
    }

    Note: I'm passing an underscore as an initial filename to the dialog. I have found that passing a value (any value) in this field allows a folder to be selected.

    C++
    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. Here is the worker thread function...
  16. If you want your worker thread to be a class member, you need to declare this function as 'static'.

    Note...

    1. The calls to 'SendMessageTimeout' broadcast the custom events used to monitor what is happening in the thread. Two of the three send text information using the function's wParam parameter.
    2. The tests for the signaled state of 'm_pEventStopped' causes the thread to respond to user input received outside of the thread.
    3. The parameter pParam receives the address of the calling object (the RecThreadView object), and allows data to be transferred between the thread and the object.
    C++
    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);
        }
    }

That's it. Happy recursing. Don't get your threads in a mess.

Inspiration and Information

I am grateful to the authors of many articles on the web and in the MSDN library which I referred to whilst writing this article.

I am especially grateful to the people who have responded to previous versions of my article and to the questions I have asked on CodeProject forums pointing out potential flaws and giving me help and encouragement, including:

  • S.H. Bouwhuis was the first person to comment on the original article, and is responsible for making me want to improve it. He/she:
    • encouraged me to do more reading about resource sharing mechanisms.
    • suggested I use thread safe CEvent objects and the WaitForSingleObject instead of boolean flags to communicate between threads.
    • indicated that the worker thread function can be either static or global.
  • David Crow for providing a more elegant worker thread heuristic, which I duly put in my code. Thanks!
  • David Delaune ('Randor') for:
    • specific help with getting the synchronization right (replacing 'Sleep' with 'WaitForSingleObject').
    • identifying why my code produced access violation errors without the 'Sleep' call I'd put in. (I.e., I was accessing a non thread-safe CString directly between threads.)
    • suggesting I use the SendMessageTimeout function to communicate back to the UI thread.
  • Stephen Hewitt for encouraging me to have a more analytical approach.
  • Chris Losinger, his answer expanding on the pros and cons of whether to make the worker thread static or global.

Besides help from the above mentioned, the production of this article involved a lot of referring to the MSDN library and search engine work. As well as recommending referring to the MSDN library for every single thing you are not sure about - you may find the following worth a look...

History

  • 2010-04-12: Major overhaul of article:
    • Extensive code rewrite to improve thread safety
    • Additions to article text discussing resource sharing mechanisms
    • A new foreword for the latest revision
    • A better approach to thread synchronization
    • Explanation of why the application is threaded referring to the UI/worker thread model
    • The new version of the code is written using VS2008 rather than VS6
    • Comprehensive use of text mapping '_T("...")' with strings
  • 2009-05-23: Updated with:
    • notes describing the use of the 'this' pointer
    • some wordings changed for clarity
    • inclusion of reference to Cilu's article on the 'this' pointer on CodeGuru
  • 2008-08-31: First posted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)