DEV Community

Cover image for Process synchronization monitors in go
Angad Sharma
Angad Sharma

Posted on

Process synchronization monitors in go

Introduction

In the most recent times, programming has taken its fifth gear by leveraging process synchronization constructs to achieve thread level optimization. Popular languages like Java, python, support multi-threading. But control flow is often blurred in the process of achieving maximum concurrent throughput.

single threaded v/s multi-threaded processes
single threaded v/s multi-threaded processes

Semaphores

Semaphores are low level constructs which mainly have two methods defined on them. Wait() and Signal() . Semaphores make sure that the critical section of your code is atomic. Which means that in essence, shared memory cohesiveness should be sequential when two threads are trying to access it at the same time.

One thread acquires the lock, performs its critical section and then releases the lock for the other threads. In the meantime, all of the other threads are waiting in queue.

semaphores in action

Monitors

A Monitor is a high level process synchronization construct which abstracts away all of the timing information. It holds conditionals, shared memory, and timing information all under the same hood.

A Monitor class is an abstract data type which contains shared data variables and procedures. The variables are private and cannot be accessed from outside of the construct, only its procedures can access the variables. Only one thread can access a monitor class object at one time.

_Monitor class_

Monitors in go

We are going to construct a monitor interface which has all of the necessary functions required. Subsequently we are going to be creating a construct that satisfies the monitor interface and define the methods on it.

Create a file main.go

package main

import (  
 "fmt"  
 "sync"  
)

type Monitor interface {  
 Wait()  
 Signal()  
 GetData() \[\]string  
 PutData(string)  
}

type Words struct {  
 mutex         \*sync.Mutex  
 wordsArray    \[\]string  
 isInitialized bool  
}

func (m \*Words) Init() {  
 m.mutex = &sync.Mutex{}  
 m.wordsArray = \[\]string{}  
 m.isInitialized = true  
}
Enter fullscreen mode Exit fullscreen mode

Here our Words struct satisfies the monitor interface. All of the member variables are private. Our task is to append words in an array atomically. Note that this is not an ideal use case for monitors but serves as a good example of the same.

Since we cannot access the member variables of the Words class we will be defining fetchers and getters on it.

  • GetData() []string : GetData function simple returns the whole array of words
  • PutData(string) : PutData function takes in a word as an argument and then atomically appends it to the array.

Now we are going to be defining the remaining functions

func (m \*Words) Wait() {  
 if m.isInitialized {  
  m.mutex.Lock()  
 }  
}

func (m \*Words) Signal() {  
 if m.isInitialized {  
  m.mutex.Unlock()  
 }  
}

func (m \*Words) GetData() \[\]string { return m.wordsArray }

func (m \*Words) PutData(word string) {  
 m.Wait()

// critical section  
 m.wordsArray = append(m.wordsArray, word)  
 // critical section done

m.Signal()  
}
Enter fullscreen mode Exit fullscreen mode

A monitor clubs together timing and control information. Here only initialized structs will be able to acquire the lock.

  • Wait() : The wait function acquires the mutex (mutual exclusion) lock if the member variables are defined
  • Signal() : The signal function releases the acquired lock so that the other threads can acquire it.

Our main function looks like this

func main() {  
 m := &Words{}  
 m.Init()

wg := &sync.WaitGroup{}  
 wg.Add(2)  
 go func() {  
  defer wg.Done()  
  m.PutData("Angad")  
 }()  
 go func() {  
  defer wg.Done()  
  m.PutData("Sharma")  
 }()  
 wg.Wait()  
 fmt.Println(m.GetData())  
}
Enter fullscreen mode Exit fullscreen mode

Here we have initialized our Words struct and all of the member variables in it. Then we have started two goroutines to append words into the array. After the operation is done, we simply print out the whole array.

Output:

[Sharma Angad]
Enter fullscreen mode Exit fullscreen mode

Application of monitors

  • Producer-consumer problem: One process produces data and the other process utilizes that data. Synchronization is required between the processes.
  • Dining-philosopher problem: K number of philosophers have chopsticks in front of them. They require 2 chopsticks to eat. They need to choose between thinking and eating according to their peers.
  • File read-write problem: Monitors can be used to prevent read-after-write, write-after-read, and write-after-write problems.

Conclusion

Monitors are useful data structures used to encapsulate all of the control information, timing information, and shared data under one roof. They are an abstraction over semaphores where we can define control statements over mutual exclusion.

Monitors

Further reading

Dining Philosopher Problem Using Semaphores

L04DB4L4NC3R/go-monitors

Difference Between Semaphore and Monitor in OS

Top comments (0)