Lazy initialization and multiton: a cheap way of creating expensive objects

Sometimes creating an object is expensive, either an object takes up a lot of resources, or costs time to initialize because of external dependencies, like web services or databases.

Then it might be a good idea to construct the object as late as possible, that is when it is actually needed, and this is where the lazy initialization pattern comes in.

You may say that it looks a lot a like singleton, the difference is that a singleton has exactly one instance over the lifetime of the application, whereas there can initialize many of these expensive objects with different states.

If we really want to do that, we will have to use a multiton. This pattern looks a lot like a singleton, the main difference is that instead of one instance of an object, we can have limited instances of an object and control and contain the creation of these objects. The instances are managed through a map or dictionary.

Lazy initialization looks like this:

This pattern consists basically of three components:

  1. The Object which is the client
  2. The ObjectProxy which handles the creation of the object, or objects
  3. The ExpensiveObject itself

The Multiton simply looks like this:

Implementation in Go

For this pattern we will study two possible implementations:

Basic manual implementation of lazy initialization without a multiton

The basic manual implementation is quite easy:

package main

import "fmt"

type ExpensiveCar struct {
	color string
}

var expensiveCar *ExpensiveCar

func createExpensiveCar(color string) *ExpensiveCar {
	if expensiveCar == nil {
		fmt.Println("Creating a car with color: ", color)
		expensiveCar = &ExpensiveCar{
			color: color,
		}
	}
	return expensiveCar
}

func main() {
	car := createExpensiveCar("red")
	fmt.Println("Car: ", *car)
}

Line by line:

  1. We define an ExpensiveCar struct with only one property: color
  2. This implementation is not thread-safe, since the expensiveCar variable could be accessed by different threads.
  3. It also allows for one kind of car to be made. Sometimes this can be sufficient

Threadsafe implementation using sync.Mutex

To address the problem with thread-safety, which can be a big issue in Go applications, we can use the sync.Mutex struct like this. This implementation also allows us to initialize more than one expensive object, since we also implement the multiton:

This a rather radical departure from our previous implementation:

  • First of all, we use the Mutex struct. ‘Mutex’ stands for ‘Mutual exclusion’ and is a way to ‘lock’ objects to prevent from other threads other than our own thread from modifying the state of the object.
  • We also define a map, with a string as the key and a pointer to an ExpensiveCar struct as the value. Here we can store the already created objects.

We will start with the preliminaries:

package main

import (
	"fmt"
	"sync"
)

Because we will be printing out some stuff, and using the sync.Mutex struct we need to import both packages.

Next we define our ExpensiveCar struct:

type ExpensiveCar struct {
	color string
}

func createCar(color string) *ExpensiveCar {
	return &ExpensiveCar{color: color}
}

This code quite self-explanatory.

Since we need to store these cars somewhere, we need the ExpensiveCarGarage:

type ExpensiveCarGarage struct {
	collection map[string]*ExpensiveCar
	mutex      sync.Mutex
}

func createGarage() *ExpensiveCarGarage {
	return &ExpensiveCarGarage{
		collection: make(map[string]*ExpensiveCar),
	}
}

Some notes:

  1. The ExpensiveGarage is our multiton. It contains a map of our cars (mapped on color for our example) and sync.Mutex struct
  2. In the createGarage() method we just initialize our map.

A detailed look the getCar() method

The GetCar() method looks like this:

func (g *ExpensiveCarGarage) getCar(color string) *ExpensiveCar {
	g.mutex.Lock()
	defer g.mutex.Unlock()
	car, carExists := g.collection[color]
	if !carExists {
		fmt.Println("Creating car with color ", color)
		car = createCar(color)
		g.collection[color] = car
	}
	return car
}

Line by line:

  1. We first lock our instance of the garage, so that no other threads can modify it.
  2. The defer statement is there, to make sure that after the method finishes, the mutex is unlocked and other threads can access our object
  3. Next we look up our car. Not the dictionary[key] not just returns the value, if there is any, but also a boolean to denote whether the key exists in the dictionary.
  4. If the key does not exist, we create the new car, and add it to the dictionary
  5. Next we return our newly produced car.

Time to test

Now we can test this thing:

func main() {
	m := createGarage()
	var wg sync.WaitGroup
	var car *ExpensiveCar
	var car2 *ExpensiveCar
	var car3 *ExpensiveCar

	wg.Add(1)
	go func() {
		defer wg.Done()
		car = m.getCar("red")
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		car2 = m.getCar("blue")
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		car3 = m.getCar("red")
	}()
	wg.Wait()

	fmt.Println(*car)
	fmt.Println(*car2)
	fmt.Println(*car3)
}

Some notes:

  1. We use a sync.WaitGroup to make sure all our cars have been initialized before moving on to the next part of our app. This is done by calling the Add() method before starting the go routine, and making sure through the defer statement that the Done() method is called to signal that the go routine has finished. Finally, call Wait() on the WaitGroup to make sure all go routines have finished.
  2. In each of the go routines we get a car of two different colors, and assign to a variable.
  3. Lastly we print out each of these variables. Notice that the red car is constructed only once.

Conclusion

Lazy initialization can be quite useful, think for example of connection pooling, where making a connection is expensive, both in time and resources. Combined with the multiton, we can build a flexible solution for the problem.

One of the improvements in the code above could be to make sure there is only a single instance of the multiton, i.e. make it a singleton, but that is something for another post.

Leave a Reply

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