Monitor Object

The Monitor Object pattern[1] encapsulates an object's synchronisation policy along with its functionality, providing a thread-safe public interface and making the object a reliable system building block.

The Monitor Object is a normal object except that it provides a threadsafe interface, one that may be called safely without external synchronisation by any number of concurrent clients. The simplest form of this pattern is embodied by a class with a public interface synchronised with a per-object mutex. The first thing each interface method does is to lock that object's mutex, and the last thing it does is to unlock the mutex. This ensures only one interface method is executing at any time.

However, another common aspect of a Monitor Object is to have more complex blocking interactions between the interface methods. These interactions are coordinated by condition variables. For example, one interface method may internally reach a point where it must wait for some condition within the object to become true before it can continue. This is the kind of situation condition variables were born to handle.

Note that as the synchronisation complexity inside a Monitor Object rises, the value of encapsulating this complexity also rises (the burden on clients remains minimal). In lieu of the Monitor Object pattern, relying on clients to coordinate synchronisation could improve performance, but scattering an object's locking policy throughout its clients is not going to be a reliable design.

Let's see some code. We'll reimplement the example from the condition variable section, replacing the std::queue with a Monitor Object.

Listing 1. MonitorQueue.hpp

template <typename DataT>
class MonitorQueue : boost::noncopyable
{
public:
    void push(const DataT& newData)
    {
        boost::mutex::scoped_lock lock(m_monitorMutex);
        m_queue.push(newData);
        m_itemAvailable.notify_one();
    }

    DataT popWait()
    {
        boost::mutex::scoped_lock lock(m_monitorMutex);

        if(m_queue.empty())
        {
            m_itemAvailable.wait(lock);
        }

        DataT temp(m_queue.front());
        m_queue.pop();
        return temp;
    }

private:
    std::queue<DataT> m_queue;

    boost::mutex m_monitorMutex;
    boost::condition m_itemAvailable;
};

Listing 2. main.cpp

MonitorQueue<char> producedChars;


void dataProducer()
{
    unsigned char c = 'A';
    for(uint i = 0; i < numCharacters;)
    {
        if((c >= 'A' && c <= 'Z'))
        {
            producedChars.push(c++);
            ++i;
        }
        else
        {
            c = 'A';
        }
    }
    producedChars.push(EOF);
}

void dataConsumer()
{
    while(true)
    {
        if(producedChars.pop() == EOF) return;
    }
}

As you can see, I parameterised the MonitorQueue with the type of data it handles (I couldn't bear to hardcode this). You should also be able to see how much simpler the clients are than in the original example. Additionally, it is now a simple matter to create new consumers or producers: we've abstracted away the synchronisation details so all this new code has to worry about is doing its specific job.

One issue to be aware of when implementing a Monitor Object is the possibility of self-deadlock, as mentioned in the deadlock section. This can be avoided by strictly following the rule that interface methods do not call other interface methods.

The Monitor Object pattern is also called "Threadsafe Passive Object" ("passive" because it runs in the client thread). The next pattern we'll cover is Active Object.

References

1. Schmidt, D., Stal, M., Rohnert, H., & Buschmann, F. (2000). Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects. Wiley.