banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【Reprint】Singleton Pattern in Go Language

title: [Reprint] Singleton Pattern in Go Language
date: 2021-08-09 16:35:33
comment: false
toc: true
category:

  • Golang
    tags:
  • Reprint
  • Go
  • Design
  • Pattern
  • Singleton
  • Error

This article is reprinted from: Singleton Pattern in Go Language | Li Wenzhou's Blog


In the past few years, the development of Go language has been amazing, and it has attracted many cross-language learners who have switched from other languages ​​(Python, PHP, Ruby) to Go language.

In the past, many developers and startups were used to using Python, PHP, or Ruby to quickly develop powerful systems, and in most cases, they didn't need to worry about how internal transactions worked or about thread safety and concurrency. Until recent years, highly concurrent systems have become popular, and now we not only need to develop powerful systems quickly, but also ensure that the developed systems can run fast enough. (We are really struggling ☺️)

For cross-language learners who are attracted by the concurrency features naturally supported by Go language, I think mastering the syntax of Go language is not the most difficult part. The most difficult part is to break through the existing thinking and truly understand concurrency and use concurrency to solve practical problems.

Go language is so easy to implement concurrency that it is incorrectly used in many places.

Common Mistakes#

There are some common mistakes, such as not considering the concurrency-safe singleton pattern. Like the example code below:

package singleton

type singleton struct {}

var instance *singleton

func GetInstance() *singleton {
	if instance == nil {
		instance = &singleton{}   // Not concurrency-safe
	}
	return instance
}

In the above case, multiple goroutines can perform the first check, and they will all create an instance of the singleton type and overwrite each other. It cannot be guaranteed which instance will be returned here, and further operations on that instance may be inconsistent with the developer's expectations.

The reason why this is not good is that if there is code that retains a reference to the singleton instance, there may be multiple instances of that type with different states, resulting in potential different code behavior. This also becomes a nightmare in the debugging process, and it is difficult to discover this error because during debugging, there are no errors due to runtime pauses, which minimizes the possibility of non-concurrency-safe execution and easily hides the developer's problems.

Aggressive Locking#

There are also many poor solutions to this concurrency safety issue. The code below can indeed solve the concurrency safety issue, but it brings other potential serious problems by turning concurrent calls to this function into serial calls through locking.

var mu Sync.Mutex

func GetInstance() *singleton {
    mu.Lock()                    // Unnecessary to lock if the instance exists
    defer mu.Unlock()

    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

In the above code, we can see that we solve the concurrency safety issue by introducing Sync.Mutex and acquiring the lock before creating the singleton instance. The problem is that we are doing too much locking here, even if we don't need to. In the case where the instance has already been created, we should simply return the cached singleton instance. This may create a bottleneck on highly concurrent code because only one goroutine can get the singleton instance at a time.

Therefore, this is not the best approach. We need to consider other solutions.

Check-Lock-Check Pattern#

In C++ and other languages, the best and safest way to ensure minimal locking and still be concurrency-safe is to use the well-known Check-Lock-Check pattern when acquiring locks. The pseudocode representation of this pattern is as follows.

if check() {
    lock() {
        if check() {
            // Perform locking-safe code here
        }
    }
}

The idea behind this pattern is that you should perform a check first to minimize any active locking, because the overhead of an IF statement is smaller than that of locking. Secondly, we want to wait and acquire the mutex lock so that only one is executing in that block at the same time. However, between the first check and acquiring the mutex lock, another goroutine may have acquired the lock, so we need to check again inside the lock to avoid being overwritten by another instance.

If we apply this pattern to our GetInstance() method, we would write code similar to the following:

func GetInstance() *singleton {
    if instance == nil {     // Not completely atomic here
        mu.Lock()
        defer mu.Unlock()

        if instance == nil {
            instance = &singleton{}
        }
    }
    return instance
}

By using the sync/atomic package, we can atomically load and set a flag that indicates whether we have initialized the instance.

import "sync"
import "sync/atomic"

var initialized uint32
... // Code omitted here

func GetInstance() *singleton {

    if atomic.LoadUInt32(&initialized) == 1 {  // Atomic operation 
		    return instance
	  }

    mu.Lock()
    defer mu.Unlock()

    if initialized == 0 {
         instance = &singleton{}
         atomic.StoreUint32(&initialized, 1)
    }

    return instance
}

But... this looks a bit cumbersome. In fact, we can do better by studying how Go language and the standard library implement goroutine synchronization.

Go Language's Idiomatic Singleton Pattern#

We want to use the idiomatic way of Go to implement this singleton pattern. We found the Once type in the standard library sync. It guarantees that an action will be performed exactly once. Here is the source code from the Go standard library (some comments have been modified).

// Once is an object that will perform exactly one action.
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 { // check
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()                          // lock
	defer o.m.Unlock()
	
	if o.done == 0 {                    // check
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

This shows that we can use this implementation to perform a function/method only once. The usage of once.Do() is as follows:

once.Do(func() {
    // Perform safe initialization here
})

Below is the complete code for implementing the singleton, which uses the sync.Once type to synchronize access to GetInstance() and ensure that our type is only initialized once.

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

Therefore, using the sync.Once package is the preferred way to safely achieve this goal, similar to how Objective-C and Swift (Cocoa) implement the dispatch_once method to perform similar initialization.

Conclusion#

When it comes to concurrent and parallel code, you need to check your code more carefully. Always have your team members perform code reviews, as such things are easy to find.

All new developers who have just switched to Go language must truly understand how concurrency safety works in order to improve their code better. Even though Go language itself has done a lot of heavy lifting by allowing you to design concurrent code with little knowledge of concurrency, in some cases, relying solely on language features is not enough, and you still need to apply best practices when developing code.

Translated from http://marcio.io/2015/07/singleton-pattern-in-go/, with some modifications for readability.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.