Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An interruptible mutex class

0.00/5 (No votes)
6 Aug 2004 1  
How to safely terminate a thread waiting on a mutex

Introduction

As developers we're rightly concerned that our applications keep running. So concerned, in fact, that it's easy to forget that a user will judge our work not just by how well it runs but also by how easily they can stop it running. Dunno about you but when I hit the close button on an application I expect it to go away now. It's acceptable that it ask me if I want to save changes I've made but it's not acceptable that it just hangs around, seemingly indefinitely. Even if the UI goes away I'll probably notice the app is still there in task manager if it takes a while to exit. (In passing I note that Visual Studio sometimes seems to take forever to terminate - hanging around long after the UI has been torn down. Interestingly enough, when it does this it consumes almost no CPU cycles. What follows is part of my educated guesses as to the reason why).

Background

Much of the work I've been doing lately involves multiple threads. I've already covered, in other articles, some of the issues involved with using threads whilst being able to stop them at will, such as using overlapped I/O[^] in order to make an I/O thread stoppable and the need to make sure a thread is cognizant of external events in this article[^ ].

Recently I ran into another issue with stopping threads. The issue involved synchronising access to a shared resource.

Sharing resources

We all know the theory. If two or more threads are sharing a resource they should implement some kind of mediation mechanism in order to be sure that changes made by a thread are completed and the shared resource returns to a stable state before another thread gains access. For example, if one thread is adding records to the tail of a linked list it's probably not a good idea to have another thread simultaneously removing records from the head of that linked list. That way memory corruption lies! Thus it behooves the careful programmer to implement locking around the operations on the linked list. Thread A aquires the lock or waits until it can aquire the lock. Once it's got the lock it performs the work it needs to do on the linked list and then releases the lock. If Thread B wants to perform operations on the linked list while Thread A has the lock it waits until the lock is released. Once the lock is released thread B has a chance to grab the lock and do it's work.

Mutexes

One way to implement the mediation mechanism is to use a mutex. The name is a contraction of mutual exclusion. One entity, and one entity alone, can own a mutex. If you own a mutex no one else can own it. As long as someone else owns it you can't. If everyone who wants to perform work on a shared resource agrees that such work can only be done when a mutex protecting the shared resource is owned by that worker the problem is solved.

You create a mutex on the Windows platform by calling the CreateMutex function, which returns a handle to a mutex object. You aquire ownership of the mutex either by specifying that you own it when you create it, or by waiting on it. If the mutex isn't owned by anyone else when you wait on it you get ownership, otherwise you wait until it's been released by the owner. And finally, when you've finished with owning the mutex you release it.

The problem

If you've read my article about using overlapped I/O you know what the problem is. The problem is that the classic way of waiting on a mutex is to use WaitForSingleObject() which, as the name suggests, waits on a single object (the mutex handle). The wait is, of course, done deep in the bowels of the operating system in code you didn't write and don't control. There are only two ways for the call to return. Either the object is signalled (we gained ownership of the mutex) or the timeout has elapsed. I might write a thread thusly,

unsigned __stdcall ThreadProc(LPVOID data)
{
    // Cast data to some type relevant to the context

    // and do some preliminary work. Then grab the mutex.

    WaitForSingleObject(gbl_hMutex, INFINITE);
    
    // Now we've got the mutex, do some work and then release it.

    ReleaseMutex(gbl_hMutex);
}
        

Now obviously this thread procedure makes no provision for the possibility that it might need to be externally terminated. The only way you're going to kill this thread, once it's waiting on the mutex, is to make sure the mutex is released so this thread can gain ownership of it (and thus break out of the WaitForSingleObject()) call, or use TerminateThread(), which is a course fraught with problems unless you're terminating the entire process. What if you want to terminate only this thread yet keep the entire process running?

The first approach might be to do something like this.

unsigned __stdcall ThreadProc(LPVOID data)
{
    bool bStayInLoop = true;
    
    while (bStayInLoop)
    {
        //  Do some preliminary work then grab the mutex

        switch (WaitForSingleObject(gbl_hMutex, 100))
        {
        case WAIT_OBJECT_0:
            //  We got the mutex, break out of the while loop

            bStayInLoop = false;
            break;
            
        case WAIT_TIMEOUT:
            //  We timed out, check if we should terminate the

            //  thread.

            if (gbl_exitThread)
                //  It's terminate  time, exit the thread

                return 0;
        }
    } 
    
    // Now we've got the mutex, do some work and then release  it.

    ReleaseMutex(gbl_hMutex);
}
        
which waits a short time for the mutex, times out if it didn't get the mutex, checks if it should exit and if not repeats the entire process. This takes a some educated guessing to get it right. Set the timeout too low and you waste CPU cycles checking if your thread should exit. Set the timeout too high and the thread takes too long to terminate itself.

A better approach

is to use the WaitForMultipleObjects() call passing two handles. One handle is the handle to the mutex you want to own, the other handle is to an event object which can be signalled when you want the thread to terminate. The code would look something like this.

unsigned __stdcall ThreadProc(LPVOID data)
{
    //  Do some preliminary work then grab the mutex

    
    HANDLE hArray[2] = { gbl_stopEvent, gbl_hMutex };
    
    switch (WaitForMultipleObjects(2, hArray, FALSE, INFINITE))
    {
    case WAIT_OBJECT_0:
        //  We've been signalled to exit, so exit

        return 0;

    case WAIT_OBJECT_0 + 1:
        //  We got the mutex

        break;
    }
    
    // Now we've got the mutex, do some work and then release  it.

    ReleaseMutex(gbl_hMutex);
}

Now you can wait for the mutex to become available, safely terminate the thread when requested and do both without wasting any CPU cycles. Naturally I want to encapsulate at least part of this into a class I can reuse.

CInterruptibleMutex

Is a class that handles the creation and destruction of the mutex and performs the wait on your behalf. The class looks like this.
class CInterruptibleMutex
{
public:
    enum eMutexState
    {
        eStopped,
        eMutexAquired,
        eTimedOut,
    };
                    CInterruptibleMutex();
    virtual         ~CInterruptibleMutex(void);

    bool            IsValid() const
                    { return m_hMutex != INVALID_HANDLE_VALUE; }

    eMutexState     AquireMutex(HANDLE hStopEvent, 
                                DWORD dwTimeout = INFINITE);
    void            ReleaseMutex() const  { ::ReleaseMutex(m_hMutex); }

private:
    HANDLE          m_hMutex;
};
        

which looks pretty straightforward. The constructor creates the mutex object, initially unowned by anyone. The destructor closes the mutex handle. The IsValid() function can be used to check that it was possible to create the mutex object controlled by the class. This will only fail if Windows was unable to allocate a handle, and if that's the case your program probably has little chance of running successfully anyway!

AquireMutex() takes a handle to an event which is used to interrupt the call if required; You can specify a timeout that defaults to INFINITE.

The AquireMutex() function looks like this.
CInterruptibleMutex::eMutexState CInterruptibleMutex::AquireMutex(
                                        HANDLE hStopEvent, DWORD dwTimeout)
{
    assert(IsValid());

    HANDLE hArray[2] = { hStopEvent, m_hMutex };

    switch (WaitForMultipleObjects(2, hArray, FALSE, dwTimeout))
    {
    case WAIT_OBJECT_0:
        return eStopped;

    case WAIT_OBJECT_0 + 1:
        return eMutexAquired;

    default:
        return eTimedOut;
    }
}
which creates a handle array and calls WaitForMultipleObjects() . The call returns the appropriate value from the eMutexState enumeration so that calling code can determine an appropriate course of action. Pretty simple.

Using the code

Add the files in the download to your project and then instantiate a CInterruptibleMutex object where you'd normally use a HANDLE to a mutex. Create an event object somewhere in your code and make sure it's a manual reset event. Then call the AquireMutex() function passing the event handle. If you need to terminate the AquireMutex() call before it aquires ownership of the mutex you signal the event handle. Of course, the calling function has to take account of the return value from AquireMutex() but most of my usage of the class has specified an infinite timeout so I never need worry about the possibility of it returning eTimedOut (I should live so long...). Thus, in the usual case, a call to AquireMutex() can be as simple as,

if (m_myMutex.AquireMutex(m_hStopEvent) == CInterruptibleMutex::eStopped)
    //  We were stopped, exit the thread

    return 0;

History

August 7th, 2004 - Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here