Thread Safe Lazy Loading Using sync.Once in Go
Jan 19, 2021
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