Introduction
Sometimes in a multi-threaded program, you need to protect a resource from concurrent access, that is access by multiple threads. One way of doing this, is to implement a Monitor Object.
To achieve this we do the following:
- We identify the resource we need to protect from being accessed by multiple threads
- Then we create a Monitor object in which we encapsulate the shared resource, and we provide method for access and modifying it.
- We can use Go’s
Mutex
struct to protect our resource, ensuring that only one thread at a time can access it. - We must also implement methods that lock the mutex before accessing the shared resource, and unlock it afterwards.
Implementation in Go
In this example we will implement a simple program to administer stocks, or in our case exactly one stock.
The Stock
struct
This is how we define the Stock
struct:
type Stock struct {
Name string
Price float64
}
func NewStock(name string, price float64) *Stock {
return &Stock{
Name: name,
Price: price,
}
}
A Stock
in our example just has a name and a price. The only method we define on this struct is a simple constructor.
Next we come to the Monitor
object:
type Monitor struct {
Value *Stock
Mutex sync.Mutex
Condition *sync.Cond
}
func NewMonitor(stock *Stock) *Monitor {
return &Monitor{
Value: stock,
Mutex: sync.Mutex{},
Condition: sync.NewCond(&sync.Mutex{}),
}
}
func (m *Monitor) UpdatePrice(price float64) {
m.Mutex.Lock()
defer m.Mutex.Unlock()
fmt.Printf("Updating price of %v to %v for stock %v\n", m.Value.Name, price, m.Value.Name)
m.Value.Price = price
m.Condition.Broadcast()
}
func (m *Monitor) WaitForRelease() {
limit := 120.0
m.Mutex.Lock()
for m.Value.Price < limit {
fmt.Printf("Waiting for price of %v to go over %v\n", m.Value.Name, limit)
m.Condition.Wait()
}
fmt.Printf("Price of %v is now %v and above %v\n", m.Value.Name, m.Value.Price, limit)
m.Mutex.Unlock()
}
A short description:
In the Monitor
struct we hold:
- A reference to the shared resource, in our case a
Stock
struct. - A
Mutex
is used to make sure only one thread at a time can access our shared resource - We use a
Cond
struct to be able to signal other threads we have finished with the resource.
In the NewMonitor
we create a new Monitor
object with the supplied Stock
struct as it value.
In UpdatePrice()
we do the following:
- We lock the
Mutex
and defer unlocking it. - Next we update the price
- Finally we signal any waiting thread that we have finished.
In WaitForRelease()
we wait for the price to go over a certain limit, till that limit is reached the thread is blocked. Once the stock reaches the limit, the thread continues executing.
Testing time
Let’s test our simple setup:
func main() {
monitor := NewMonitor(NewStock("GOOG", 110.0))
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
monitor.UpdatePrice(110.0 + float64(i)*2.0)
}()
}
go monitor.WaitForRelease()
wg.Wait()
fmt.Printf("Stock is now %+v\n", monitor.Value)
}
This is what happens in main:
- We create a new
Monitor
object with aNewStock
struct as its value. - Next we start ten go routines, each of which will try to update the price.
- After this we wait for the price to go over 120.
- Finally we print out the final price of the stock.
Conclusion
Since Go was built for multi-threaded applications, writing this pattern was quite easy and it turned out to be quite straightforward. Especially using the Cond
to signal between go routines make life quite easy.
One caveat: this is a simple example. In a later article I will build a more elaborate example where I will also address the possiblity of data-races.
Another enhancement would be to make the Monitor
struct more generic.