The Monitor Pattern
Today I want to discuss the use of locks to make an object thread safe. That is, by employing a simple pattern for locking access to your object, you can effectively guarantee that only a single thread has access to any part of the object at a time. This, therefore, guarantees that the state of the object can never be corrupted, even when multiple threads attempt to access it concurrently.
Object-oriented Design
Before discussing concurrency with shared, mutable objects, let’s first consider what makes for a well-designed object in general. A newby mistake in object-oriented design is to think of objects as being containers of information. When designed this way, objects get mapped based on the data they contain and there is a temptation to expose that data through a series of getters and setters. A well-designed object, however, will contain state as a means of encapsulation functionality which needs that state to operate. It does this by allowing other objects to send it messages via methods to perform those operations which may update the internal state as a side-effect.
As a contrived example, if we’re building a linked list with only a push operation, we could build a simple list like this:
class LinkedListRaw {
has $.data;
has $.next is rw;
}
my $list = LinkedListRaw.new(data => 1);
$list.next = LinkedListRaw.new(data => 2);
$list.next.next = LinedListRaw.new(data => 3);
Whereas a much better design would be more like this:
class LinkedList {
has $.data;
has $!next;
method !tail() {
my $here = self;
loop { return $here without $here.next; $here = $here.next }
}
method push($data) {
self!tail.next = LinkedList.new(:$data);
}
}
my $list = LinkedList.new(data => 1);
$list.push: 2;
$list.push: 3;
Now, the reason I took a moment to mention good object-oriented design practice is because the monitor pattern we will now consider relies on well-designed objects to be effective. (I also mention it because bad object-oriented design practices are pervasive even among otherwise good engineers.)
Monitors
Concurrency is easiest when you have a stateless system or a system whose state is based on transforming immutable objects. For example, if you just need to feed calculations from one thread to another, it’s very easy for each stage to keep a copy of the current value, transform that value, and pass a new copy on to the next stage.
However, stateful objects can present a challenge because a change to the object’s state might be only partially applied in one thread when another thread begins a new operation with it. If we don’t protect our object from performing multiple state changes at once or from reads while a state change is only partially complete, our code will not be thread safe. You should never use such an object from multiple threads at once. (Most built-in Raku objects are such objects!)
If you are in a situation were copying your object state is not practical and you need to share state between threads, a very easy solution is to employ the monitor pattern. Here’s a thread safe version of our push-only linked list from before using the monitor pattern:
class LinkedListSafe {
has $.data;
has $!next;
has Lock $!lock .= new;
method !tail() {
my $here = self;
loop { return $here without $here.next; $here = $here.next }
}
method push($data) {
$!lock.protect: {
self!tail.next = LinkiedListSafe.new(:$data);
}
}
}
That’s it. That’s the whole monitor pattern, just using a Lock
to protect all
thec code of every public method that reads or writes from the mutable state of
the object. While this is easy, there are a couple of downsides to employing
this pattern:
-
This is generally a low-performing solution if contention for state changes is high. For example, if many threads will be performing many pushes frequently on this linked list, the performance will not be very good.
-
Adding the
$!lock.protect: { ... }
bit around every section is tedious to do and easy to forget during development.
To improve the first situation make sure that your monitor only contains the code related to encapsulating object state. Create secondary objects that are not monitors for any calculations and actions that are not monitors for any other stateless work.
For the second, I recommend using a module by Jonathan Worthington. He has written a tool to automate the implementation of the monitor pattern. If you install OO::Monitors, you can rewrite the above linked list as:
use OO::Monitors;
monitor LinkedListMonitor {
has $.data;
has $!next;
method !tail() {
my $here = self;
loop { return $here without $here.next; $here = $here.next }
}
method push($data) {
self!tail.next = LinkiedListSafe.new(:$data);
}
}
A monitor
is a class
implementing the monitor pattern. Every method is
automatically protected by a lock for you.
In cases where you need to make a stateful object thread safe and you want a simple mechanism for doing it, this is a reasonable pattern to follow. If performance is a primary concern, a monitor object may not be for you, though. Finally, be aware that this pattern depends on thoughtful OO design to work at all.
Cheers.