Go 单例模式

转载:https://blog.csdn.net/jiaolongdy/article/details/79450475

最近几年go语言的增长速度非常惊人,吸引着各界人士切换到Go语言。最近有很多关于使用Ruby语言的公司切换到Go、体验Go语言、和Go的并行和并发解决问题的文章。

过去10年里,Ruby on Rails已经让众多的开发者和初创公司快速开发出强大的系统,大多数时候不需要担心他的内部是如何工作的,或者担心线程安全和并发。RoR进程很少创建线程和并行的运行一些东西。整个托管的基础建设和框架栈使用不同的方法,通过多个进程来进行并行。最近几年,像Puma这样的多线程机架式服务器开始流行,但是即使是这样,刚开始也带来了很多关于使用第三方gems和其他没有被设计为线程安全的代码的问题。

现在有很多开发者开始使用Go语言。我们需要仔细研究我们的代码,并观察代码的行为,需要以线程安全的方式代码设代码。

常识性错误

最近,我在很多Github库里看到这种类型的错误,单例模式的实现没有考虑线程安全,下面是常识性错误的代码

package singleton

type singleton struct {
}

var instance * singleton

func GetInstance() * singleton {
     if instance == nil {
        instance = &singleton{}    // <---非线程安全的
    }
     return instance
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面的示例中,多个go routines 会进行第一次检查并且都会创建singleton类型的实例并且互相覆盖。不能保证哪一个实例会被返回,在这个实例上更进一步的操作可能和开发者所期望的不一至。

这样是有问题的,因为如果对这个单例的实例已经在代码中被应用,可能会有潜在的多个这个类型的实例,并用有各自的状态,产生潜在的不同的代码行为。他也可能成为高度时的恶梦,并且很难定位错误,因为在debug时由于运行时暂停减少潜在的非线程安全的执行而不会真正出现错误,很容易隐藏开发者的问题。 
激进的锁

我也看到一些使用糟糕的方法来解决线程安全的问题。事实上他解决了多线程的问题,但是创造了其他潜在的更严重的问题,他通过对整个方法执行锁定来引入线程竞争

var mu Sync.Mutex

func GetInstance() * singleton {
    mu.Lock()                     // <---如果实例已经被创建就没有必要锁写
    defer mu.Unlock()

    if instance == nil {
        instance = & singleton{}
    }
    return instance
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上面的代码,我们可以看到,通过引入Sync.Mutex来解决线程安全的问题,并且在创建单例实例前获取锁。问题在于当我们不需要的时候例如,实例已经被创建的时候,只需要返回缓存的单例实例,但是呢也会执行锁操作。在高并发代码基础上,这会产生瓶颈,因为在同一时间只有一个go routine可以得到单例的实例。

所以这不是最好的方法,我们找找其他的解决方案。

Check-Lock-Check 模式

在c++和其他语言,用于保证最小锁定并且保证线程安全的最好、最安全的方式是当需要锁定时使用众所周知的Check-Lock-Check模式。下面的伪代码说明了这个模式的大概样子

扫描二维码关注公众号,回复: 5977646 查看本文章
if check() {
     lock () {
         if check() {
             // perform your lock-safe code here 
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这个模式背后的想法是想一开始就检查。用于减少任何激进的锁定。因为一个IF语句比锁定便宜的多。第二我们想等待并获取排他锁所以的块内同一时间只能有一个执行,但是在第一次检察和和排他锁获取之间可能会有其他线程想要获取锁,因此我们需要在块内再次的检查以避免单例实例被其他实例替换。

多年来,和我一起工作人的熟知这一点,在代码审过程中,这个模式和线程安全思想方面,我对团队非常严厉。

如果我们应用这个模式到我的GetInstance()方法,我们需要做的如下:

func GetInstance() * singleton {
     if instance == nil {      // <--不够完善.他并不是完全的原子性
        mu.Lock()
        defer mu.Unlock()

        if instance == nil {
            instance = & singleton{}
        }
    }
    return instance
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

   
这是一个挺好的方法,但是并不完美。因为编译器优化,但是没有实例保存的状态的原子性检查。全面的技术考虑,这并不是安美的。但是已经比之前的方法好多了。

但是使用sync/atomic 包,我们可以原子性的加载和设置标识指示是否已经初始化了我们的实例。

import " sync " 
import " sync/atomic "

var initialized uint32
...

func GetInstance() * singleton {

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

    mu.Lock()
    defer mu.Unlock()

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

    return instance
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

但是…..我相信我们可以通过查看Go语言和标准库的源码看一下go routines 同步的实现方式来做的更好

Go惯用的单例方法

我们想要使用Go的惯用手法来实现这个单例模式。所以我们需要看一下打包好的sync标准库。我们找到了Once 类型。这个对象可以精确的只执行一次操作,下面就是Go标准库的代码

// Once is an object that will perform exactly one action. 
type Once struct {
    m Mutex
    done uint32
}

 // Do calls the function f if and only if Do is being called for the
 // first time for this instance of Once. In other words, given
 //      var once Once
 // if once.Do(f) is called multiple times , only the first call will invoke f,
 // even if f has a different value in each invocation. A new instance of
 // Once is required for each function to execute.
 // 
 // Do is intended for initialization that must be run exactly once. Since f
 // is niladic, it may be necessary to use a function literal to capture the
 // arguments to a function to be invoked by Do:
 //     config.once.Do(func() { config.init(filename) })
 // 
 // Because no call to Do returns until the one call to f returns, if f causes
 // Do to be called, it will deadlock.
 // 
 // If f panics, Do considers it to have returned; future calls of Do return
 // without calling f.
 //
 func (o * Once) Do(f func()) {
     if atomic.LoadUint32(&o.done ) == 1 { // <-- Check 
        return
    }
    // Slow-path. 
    omLock()                            // <-- Lock 
    defer omUnlock()
     if o.done == 0 {                      // <-- Check 
        defer atomic.StoreUint32(&o.done, 1 )
        f()
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

这意味着我们可以运用非常棒的 Go sync包来调用一个只执行一次的方法。因此,我们可以向下面这样调用once.Do() 方法

once.Do(func() {
     // 执行安全的初始化操作 
})
  • 1
  • 2
  • 3

下面你可以看到使用sync.Once类型实现的单例实现的完整代码,用于同步访问GetInstance() 并保证我们的类型初始化只执行一次。

package singleton

import (
    " sync "
)

type singleton struct {
}

var instance * singleton
 var once sync.Once

func GetInstance() * singleton {
    once.Do(func() {
        instance = & singleton{}
    })
    return instance
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

因此,使用sync.Once包是一个完美的安全的实现方式,这种方式有点像Object-C和Swift(Cocoa)的实现dispatch_once 方法用于执行类似的初始化操作。

总结

当涉及到并行和并发代码时,需要详细检查你的代码。始终让你的团队成员进行代码审查,因此对于这样的事情才能更容易的监督。

所有的切换到Go语言的新开发者,需要明确的理解线程安全的原理才能更好的改善你的代码。即使Go语言本身通过做了很多努力允许你使用很少的并发知识来设计并发代码。仍有一些语言无法帮你处理的一些情况,你依然需要在开发代码时应用最佳的实践方法

补充

  1. 为什么不使用 init()函数声明单例?这样更简单高效 
    答: 如果想要实现懒初始化(Lazy initialization)则需要考虑同步问题
  2. sync.Once 会保证又一次调用成功之后再返回,也就是说如果多人同时调用Do函数,则只有一个人调用成功之后,其他人才会返回。
  3. 当调用函数Panic时,也会认为其已经调用完成,其它调用方也会返回。

原文地址

另一篇:你真的会用go语言写单例模式吗?

https://www.cnblogs.com/jian-99/p/8761374.html

最近在学习Golang,想着可以就以前的知识做一些串通,加上了解到go语言也是面向对象编程语言之后。在最近的开发过程中,我碰到一个问题,要用go语言实现单例模式。本着“天下知识,同根同源”(我瞎掰的~),我心想,这有什么难的,可是真正做起来,还是碰到了不少问题。

  下面是我的经历:

  1.我先是完成了我的第一版单例模式,就是非并发,最简单的一种,懒汉模式:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

var instance *single

type single struct{

    Name string

}

func GetInstance()*single{

    if m == nil{

        m = &single{}

    }

    return m

}

func main(){

    a := GetInstance()

    a.Name = "a"

    b := GetInstance()

    b.Name = "b"

    fmt.Println(&a.Name, a)

    fmt.Println(&b.Name, b)

    fmt.Printf("%p %T\n", a, a)

    fmt.Printf("%p %T\n", b, b)

}

  结果如下:

0xc04203e1b0 &{b}
0xc04203e1b0 &{b}
0xc04203e1b0 *main.single
0xc04203e1b0 *main.single

  可以看到,我们已经实现了简单的单例模式,我们申请了两次实例,在改变一个第二个实例的字段之后,第一个也随之改变了。而且从他们的地址都相同也可以看出是同一个对象。但是,这样简陋的单例模式在并发下就容易出错,非线程安全的。

  现在我们是在并发的情况下去调用的 GetInstance函数,现在恰好第一个goroutine执行到m = &Manager {}这句话之前,第二个goroutine也来获取实例了,第二个goroutine去判断m是不是nil,因为m = &Manager{}还没有来得及执行,所以m肯定是nil,现在出现的问题就是if中的语句可能会执行两遍!

  2.紧接着我们做了一些改进,给单例模式加了锁:

1

2

3

4

5

6

7

8

9

10

11

12

13

var m *single

var lock sync.Mutex

type single struct{

    Name string

}

func GetInstance()*single{

    lock.Lock()

    defer lock.Unlock()

    if m == nil{

        m = &single{}

    }

    return m

}

  结果同上。

  与此同时,新的问题出现了,在高并发环境下,现在不管什么情况下都会上一把锁,而且加锁的代价是很大的,有没有办法继续对我们的代码进行进一步的优化呢?

  3.双重锁机制:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

var m *single

var lock sync.Mutex

type single struct{

    Name string

}

func GetInstance()*single{

    if m == nil{

        lock.Lock()

        defer lock.Unlock()

        if m == nil{

            m = &single{}

        }

    }

    return m

}

  这次我们用了两个判断,而且我们将同步锁放在了条件判断之后,这样做就避免了每次调用都加锁,提高了代码的执行效率。理论上写到这里已经是很完美的单例模式了,但是我们在go语言里,我们有一个很优雅的写法。

  4.sync包里的Once.Do()方法

1

2

3

4

5

6

7

8

9

10

11

12

var m *single

var once sync.Once

type single struct{

    Name string

}

func GetInstance()*single{

    once.Do(func() {

        m = &single{}

    })

    return m

}

  Once.Do方法的参数是一个函数,这里我们给的是一个匿名函数,在这个函数中我们做的工作很简单,就是去赋值m变量,而且go能保证这个函数中的代码仅仅执行一次!

  以后在用go语言写单例模式的时候,可不要再傻傻的去使用前面那些例子了,既然已经有了优雅又强大的方法,我们直接用就完了。

https://blog.csdn.net/qibin0506/article/details/50733314

  • pirlo-san: Go语言无法完美支持单例模式,Client还是可以通过如下方式生成多个实例:
     
    1.  
    2. m := Manager{}

    3.  
    (4周前#14楼)

    0

  • u010824081

    SergeyChang: Manager 结构体是导出的,假设调用方不用你给出的构造函数呢?(9个月前#13楼)收起回复

    0

    • wh5pyw78

      wh5pyw78回复 SergeyChang: 对,我也想问这个问题,博主的Manager本身就定义成了首字母大写的,全局都可见,别人随便就new了一个,根本不去调用你的GetInstance函数,你怎么办?(7个月前)

  • hzwy23: 大神您好,看了下您这段代码,想了想,golang没有默认构造函数这个玩意。不像java将构造函数私有化,这样就可以防止别的地方直接new一个对象,而这单代码中type Manager struct {}。在别的包,可以直接new出来。 在下写了一段代码,请大神帮忙看看。 package instance var M *manager type manager struct { } func (this *manager) SayToMe(str string) string { return &quot;This is SigleTon :&quot; + str } func init() { M = new(manager) } 将这个包单独的放在一个文件夹中。防止其他包直接访问struct manager 不知道这种方式是否实现了golang的单利模式。请大神指点。(2年前#7楼)收起回复

    0

    • qibin0506

      亓斌回复 hzwy23: 可以,利用golang的可见性特性, 其实就是java的private(2年前)

猜你喜欢

转载自blog.csdn.net/AlbertFly/article/details/89011315
今日推荐