Mastering Concurrent Harmony: Easy Implementation of the Guarded Suspension Pattern in Go

Photo by Travis Saylor: https://www.pexels.com/photo/cyclone-fence-in-shallow-photography-951408/

Introduction

In multithreaded applications, it’s common for one thread to let another know when specific conditions are met, like when data is ready or a time-consuming task is finished. In Go, you can use goroutines and channels to achieve this. This example demonstrates implementing a GuardedGarage to park ExpensiveCar objects.

Implementation in Go

We will start by importing the necessary packages:

import (
	"fmt"
	"time"
)

Next we define our ExpensiveCar struct:

type ExpensiveCar struct {
	brand string
	color string
}

func CreateCar(brand string, color string) *ExpensiveCar {
	return &ExpensiveCar{brand: brand, color: color}
}

This code is quite self-explanatory.

Let’s have a look at some more interesting code, the GuardedGarage:

type GuardedGarage struct {
	ch   chan *ExpensiveCar
}

func NewGuardedGarage() *GuardedGarage {
	g := &GuardedGarage{}
	g.ch = make(chan *ExpensiveCar)
	return g
}

func (g *GuardedGarage) Put(c *ExpensiveCar) {
	g.ch <- c
	fmt.Println("Putting", c)
}

func (g *GuardedGarage) Get() *ExpensiveCar {
	return <-g.ch
}

A few notes here:

  1. In the GuardedGarage we keep
    • A channel on which we can send our parked cars.
  2. There is a simple constructor-like method
  3. In the Put() method, we lock our code, put the car to the channel and return
  4. The Get() method just returns what is on the channel. There is no need to lock the code here, it will even give an error if you do.

Why we don’t a lock in the Get() method?

The Get() method doesn’t need to be locked as it is using a channel to synchronize operation. When we put a car in the garage we send it to the channel. When we dequeued it, the channel receives it. Channel operations are synchronized which means the Put() method will not continue unless the Get() method has received a car. This way channels are thread-safe and there is no need for an explicit lock.

Testing time

Now we can test our setup:

func main() {
	garage := NewGuardedGarage()
	go func() {
		garage.Put(CreateCar("BMW", "red"))
		garage.Put(CreateCar("Audi", "blue"))
		garage.Put(CreateCar("Mercedes", "green"))
		close(garage.ch)
	}()

	fmt.Println(garage.Get())
	fmt.Println(garage.Get())
	fmt.Println(garage.Get())
}

We create a garage, start a go routine which fills the garage, and the try and get some cars from it. In the output you will probably see something like this:

Putting &{BMW red}
&{BMW red}               
&{Audi blue}             
Putting &{Audi blue}     
Putting &{Mercedes green}
&{Mercedes green}   

The puts and gets seem to be out of order, but that has to do with the multi-threaded nature of this pattern.

Conclusion

It took a while for me to understand this pattern, but it turned to be quite easy using channels, which are by their very nature thread-safe. One possible enhancement could be to have multiple consumers for our GuardedGarage instead of just one. Also, make this pattern more generic could be another enhancements.

Leave a Reply

Your email address will not be published. Required fields are marked *