Monitor Object
The monitor object design pattern synchronizes concurrent member function execution to ensure that only one member function at a time runs within an object. It also allows object’s member functions to schedule their execution sequences cooperatively. (Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects)
The Monitor Object design pattern synchronizes concurrent member function execution to ensure that only one member function runs within an object at a time. It also allows an object’s member functions to schedule their execution sequences cooperatively.
Also know as
- Thread-Safe Passive Object
Problem
If many threads access a shared object concurrently, the following challenges exist.
- Due to the concurrent access, the shared object must be protected from non-synchronized read and write operations to avoid data races.
- The necessary synchronization should be part of the implementation, not the interface.
- When a thread is done with the shared object, a notification should be triggered so the next thread can use the shared object. This mechanism helps avoid and improves the system’s overall performance.
- After the execution of a member function, the invariants of the shared object must hold.
Solution
A client (thread) can access the Monitor Object’s synchronized member functions, and due to the monitor lock, only one synchronized member function can run at any given time. Each Monitor Object has a monitor condition that notifies the waiting clients.
Components
- Monitor Object: The Monitor Object supports one or more member functions. Each client must access the object through these member functions, and each member function runs in the client’s thread.
- Synchronized member functions: The Monitor Object supports the synchronized member functions. Only one member function can execute at any given point in time. The Thread-Safe Interface helps to distinguish between the interface member functions (synchronized member functions) and the implementation member functions of the Monitor Object.
- Monitor lock: Each Monitor Object has one monitor lock, which ensures that at most one client can access the Monitor Object at any given time.
- Monitor condition: The monitor condition allows separate threads to schedule their member function invocations on the Monitor Object. When the current client is done with its invocation of the synchronized member functions, the next waiting client is awakened to invoke the Monitor Object’s synchronized member functions.
While the monitor lock ensures the synchronized member functions’ exclusive access, the monitor condition guarantees minimal waiting for the clients. Essentially, the monitor lock protects from data races and the condition monitor from deadlocks.
Dynamic Behavior
The interaction between the Monitor Object and its components has different phases.
- When a client invokes a synchronized member function on a Monitor Object, it must first lock the global monitor lock. If the client successfully locks, it executes the synchronized member function and unlocks the monitor lock. If the client is not successful, the client is blocked.
- When the client is blocked because it cannot progress, it waits until the monitor condition sends a notification. This notification happens when the monitor is unlocked. The notification can be sent to one or all the waiting clients. Typically, waiting means resource-friendly sleeping in contrast to busy waiting.
- When a client gets the notification to resume, it locks the monitor lock and executes the synchronized member function. The monitor lock is unlocked at the end of the synchronized member function. The monitor sends a notification to signal that the next client can execute its synchronized member function.
Pros and Cons
What are the advantages and disadvantages of the Monitor Object?
Pros
- The client is not aware of the implicit synchronization of the Monitor Object, and the synchronization is fully encapsulated in the implementation.
- The invoked synchronized member functions will eventually be automatically scheduled. The notification/waiting mechanism of the monitor condition behaves as a simple scheduler.
Cons
- It is often quite challenging to change the synchronization mechanism of the synchronization member functions because the functionality and the synchronization are strongly coupled.
- When a synchronized member function invokes directly or indirectly the same Monitor Object, a deadlock may occur.
Example
The following example defines a ThreadSafeQueue
.
// monitorObject.cpp #include <condition_variable> #include <functional> #include <queue> #include <iostream> #include <mutex> #include <random> #include <thread> class Monitor { public: void lock() const { monitMutex.lock(); } void unlock() const { monitMutex.unlock(); } void notify_one() const noexcept { monitCond.notify_one(); } template <typename Predicate> void wait(Predicate pred) const { // (10) std::unique_lock<std::mutex> monitLock(monitMutex); monitCond.wait(monitLock, pred); } private: mutable std::mutex monitMutex; mutable std::condition_variable monitCond; }; template <typename T> // (1) class ThreadSafeQueue: public Monitor { public: void add(T val){ lock(); myQueue.push(val); // (6) unlock(); notify_one(); } T get(){ wait( [this] { return ! myQueue.empty(); } ); // (2) lock(); auto val = myQueue.front(); // (4) myQueue.pop(); // (5) unlock(); return val; } private: std::queue<T> myQueue; // (3) }; class Dice { public: int operator()(){ return rand(); } private: std::function<int()> rand = std::bind(std::uniform_int_distribution<>(1, 6), std::default_random_engine()); }; int main(){ std::cout << '\n'; constexpr auto NumberThreads = 100; ThreadSafeQueue<int> safeQueue; // (7) auto addLambda = [&safeQueue](int val){ safeQueue.add(val); // (8) std::cout << val << " " << std::this_thread::get_id() << "; "; }; auto getLambda = [&safeQueue]{ safeQueue.get(); }; // (9) std::vector<std::thread> addThreads(NumberThreads); Dice dice; for (auto& thr: addThreads) thr = std::thread(addLambda, dice()); std::vector<std::thread> getThreads(NumberThreads); for (auto& thr: getThreads) thr = std::thread(getLambda); for (auto& thr: addThreads) thr.join(); for (auto& thr: getThreads) thr.join(); std::cout << "\n\n"; }
The key idea of the example is that the Monitor Object is encapsulated in a class and can, therefore, be reused. The class Monitor
uses a std::mutex
as monitor lock and std::condition_variable
as monitor condition. The class Monitor
provides the minimal interface that a Monitor Object should support.
ThreadSafeQueue
in line (1) extends std::queue
in line (3) with a thread-safe interface. ThreadSafeQueue
derives from the class Monitor
and uses its member functions to support the synchronized member functions add
and get. The member functions add
and get
use the monitor lock to protect the Monitor Object, particularly the non-thread-safe myQueue
. add
notifies the waiting thread when a new item was added to myQueue
. This notification is thread-safe. The member function get
(line (3)) deserves more attention. First, the wait
member function of the underlying condition variable is called. This wait
call needs an additional predicate to protect against spurious and lost wakeups (C++ Core Guidelines: Be Aware of the Traps of Condition Variables). The operations modifying myQueue
(lines 4 and 5) must also be protected because they can interleave with the call myQueue.push(val)
(line 6). The Monitor Object safeQueue
line (7) uses the lambda functions in lines (8) and (9) to add or remove a number from the synchronized safeQueue
. ThreadSafeQueue
itself is a class template and can hold values from an arbitrary type. One hundred clients add 100 random numbers between 1 – 6 to safeQueue
(line 7), while hundred clients remove these 100 numbers concurrently from the safeQueue
. The output of the program shows the numbers and the thread ids.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
With C++20, the program monitorObject.cp
p can be further improved. First, I include the header <concepts>
and use the concept std::predicate
as a restricted type parameter in the function template wait
(line 10). The concept std::predicate
ensures that the function template wait
can only be instantiated with a predicate. Predicates are callables that return a boolean as a result.
template <std::predicate Predicate> void wait(Predicate pred) const { std::unique_lock<std::mutex> monitLock(monitMutex); monitCond.wait(monitLock, pred); }
Second, I use std::jthread
instead of std::thread
. std::jthread
s an improved std::thread
in C++20 that automatically joins in its destructor if necessary.
int main() { std::cout << '\n'; constexpr auto NumberThreads = 100; ThreadSafeQueue<int> safeQueue; auto addLambda = [&safeQueue](int val){ safeQueue.add(val); std::cout << val << " " << std::this_thread::get_id() << "; "; }; auto getLambda = [&safeQueue]{ safeQueue.get(); }; std::vector<std::jthread> addThreads(NumberThreads); Dice dice; for (auto& thr: addThreads) thr = std::jthread(addLambda, dice()); std::vector<std::jthread> getThreads(NumberThreads); for (auto& thr: getThreads) thr = std::jthread(getLambda); std::cout << "\n\n"; }
The Active Object and the Monitor Object are similar but distinct in a few important points. Both architectural patterns synchronize access to a shared object. The member functions of an Active Object are executed in a different thread, but the Monitor Object member functions in the same thread. The Active Object decouples its member function invocation better from its member function execution and is, therefore, easier to maintain.
What’s Next?
DONE! I have written around 50 posts about patterns. In my next posts, I will write about unknown features in C++17, dive deeper into C++20, and present the upcoming new C++23 standard. I will start this journey with C++23.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!