fareez.info

Thread Safe Lazy Loading Using sync.Once in Go

Imagine you have a server and you are loading some configuration for executing some business logic. You don’t want to load all the configuration when the server is launched since it will take a lot of time for it to be ready for handling requests. You have to postpone loading configuration till it is actually needed. It’s called Lazy Loading.

package main

import (
	"log"
	"time"
)

var config map[string]string

func loadConfig() {
	// Adding delay to simulate as if
	// data is read from database or file
	time.Sleep(100 * time.Millisecond)
	log.Println("Loading configuration")
	config = map[string]string{
		"hostname": "localhost",
	}
}

func getConfig(key string) string {
	if config == nil {
		loadConfig()
	}
	return config[key]
}

Above program is the general structure for lazy loading. We call loadConfig only when we find config to be nil.

Now in a world where the requests are handled concurrently and each handled by its own goroutine, there is a good chance that loadConfig can be called multiple times which is not desirable.

func doSomething(done chan struct{}) {
	getConfig("hostname")
	done <- struct{}{}
}

func main() {
	done := make(chan struct{})
	for i := 0; i < 5; i++ {
		go doSomething(done)
	}
	for i := 0; i < 5; i++ {
		<-done
	}
}

We have five goroutines here racing to get the configuration. Following is the output of the program.

2021/01/18 17:50:49 Loading configuration
2021/01/18 17:50:49 Loading configuration
2021/01/18 17:50:49 Loading configuration
2021/01/18 17:50:49 Loading configuration
2021/01/18 17:50:49 Loading configuration

loadConfig is called multiple times because all the calls to getConfig happen parallely and all of them find config to be nil. Only when loadConfig completes its execution at least once we will have a non-nil value in config.

To solve this, we have sync.Once which will make sure a piece of code is executed exactly once and other goroutines have to wait, if they need to run the same piece of code. Once the first called execution completed, the remaining go routines continue to run.

package main

import (
	"log"
	"time"
	"sync"
)

var config map[string]string

var configOnce sync.Once = sync.Once{}

func loadConfig() {
	// Adding delay to simulate as if
	// data is read from database or file
	time.Sleep(100 * time.Millisecond)
	log.Println("Loading configuration")
	config = map[string]string{
		"hostname": "localhost",
	}
}

func getConfig(key string) string {
	if config == nil {
		// Execution of loadConfig is taken care
		// by the sync.Do method, we just have to pass
		// the reference to the function
		sync.Do(loadConfig)
	}
	return config[key]
}

When running the above code it produces the following output where loadConfig is executed only once

2021/01/18 17:55:09 Loading configuration
comments powered by Disqus