Go设计模式(一)从单例模式说起

2023年 10月 10日 66.3k 0

我们这里讨论的单例模式(Singleton)是懒汉模式,即在实际需要的时候才开始初始化,双重检查锁是一种比较通用的懒汉单例模式的实现方式,所以这里我们从双重检查锁(Double Check Lock)说起。

第一个版本

下面这段代码是一个标准的双重检查锁,我们看下面这种写法会有什么问题?

// singleton v0

type Singleton struct{}



var (

   instance *Singleton

   lock     sync.Mutex

)



func GetInstance() *Singleton {

   if instance == nil {

      lock.Lock()

      defer lock.Unlock()

      if instance == nil {

         instance = &Singleton{}

      }

   }

   return instance

}

问题

  • 指令重排序
  • instance = &Singleton{}这个语句,包括分配内存、内存初始化、指针赋值三个操作,这里内存初始化和指针赋值可能会发生指令重排序,可能导致第17行返回的instance没有完成内存初始化,而其他线程就会获得一个没有完成初始化的对象

  • golang赋值不保证原子性
  • 这里敲黑板,golang里唯一保证原子性操作的是atomic包,其他任何操作都不能保证,所以instance == nil这个读操作是没法保证原子性的,而可能返回一个半初始化的指针

    解决方法

  • 读写锁

  • 我们用一把读写锁来保证读取instance的操作不会被指令重排序和非原子操作所影响

    type Singleton struct{}
    
    
    
    var (
    
       instancev1 *Singleton
    
       lockv1     sync.RWMutex
    
    )
    
    
    
    func GetInstancev1() *Singleton {
    
       lockv1.RLock()
    
       ins := instancev1
    
       lockv1.RUnlock()
    
       if ins == nil {
    
          lockv1.Lock()
    
          defer lockv1.Unlock()
    
          if instancev1 == nil {
    
             instancev1 = &Singleton{}
    
          }
    
       }
    
       return instancev1
    
    }
    
  • atomic

  • 我们用一个原子赋值来保证只有当instance指针被完整赋值以后,才能被读到

    type Singleton struct{}
    
    
    
    var (
    
       instancev2 *Singleton
    
       lockv2     sync.Mutex
    
       done       uint32
    
    )
    
    
    
    func GetInstancev2() *Singleton {
    
       if atomic.LoadUint32(&done) == 0 {
    
          lockv2.Lock()
    
          defer lockv2.Unlock()
    
          if done == 0 {
    
             defer atomic.StoreUint32(&done, 1)
    
             instancev2 = &Singleton{}
    
          }
    
       }
    
       return instancev2
    
    }
    
  • sync.Once

  • 实际上golang标准库里的sync.Once就是采用atomic实现的双重检查锁,所以golang里的最佳实践是直接使用sync.Once,这里敲下黑板

    type Singleton struct{}
    
    
    
    var (
    
       instancev3 *Singleton
    
       once       sync.Once
    
    )
    
    
    
    func GetInstancev3() *Singleton {
    
       once.Do(func() {
    
          instancev3 = &Singleton{}
    
       })
    
       return instancev3
    
    }
    
  • 性能比较

  • 上面两种方法(读写锁和atomic)看上去都能很好的实现单例模式,可是你有没有想过这两种方式的性能孰高孰低?

    我们看一下实际的性能表现

    // 读写锁
    
    BenchmarkGetInstancev1-8           26312532                45.0 ns/op
    
    // atomic
    
    BenchmarkGetInstancev2-8           348387658                 3.29 ns/op
    
    // sync.Once
    
    BenchmarkGetInstancev3-8           723899626                 1.55 ns/op
    

    引申两个问题

  • 乐观锁

  • atomic方案比读写锁的方案高效,原因就在于前者本质上是一把乐观锁,加锁读的开销要远大于乐观读。

  • 内联函数

  • 细心的读者可能已经发现,上面sync.Once和atomic这两种方案本质是一样的,但是sync.Once的性能却更高,啥原因呢,what? 我们看下sync.Once的源码,就能发现,其实他利用了编译器优化,编译器会将规模比较小的代码做内联(inlining)处理,省去了函数调用的开销。之所有将doSlow单独封装成一个独立函数,是因为编译器不会将包含defer语句的函数做内联优化,所以doSlow单独封装后,Do函数就会被内联优化,因为doSlow只会被执行一次,后续都是调用Do就返回,所以Do的内联优化了函数调用的开销。

    func (o *Once) Do(f func()) {
    
       // Note: Here is an incorrect implementation of Do:
    
     //
    
     // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    
     //    f()
    
     // }
    
     //
    
     // Do guarantees that when it returns, f has finished.
    
     // This implementation would not implement that guarantee:
    
     // given two simultaneous calls, the winner of the cas would
    
     // call f, and the second would return immediately, without
    
     // waiting for the first's call to f to complete.
    
     // This is why the slow path falls back to a mutex, and why
    
     // the atomic.StoreUint32 must be delayed until after f returns.
    
    
    
     if atomic.LoadUint32(&o.done) == 0 {
    
          // Outlined slow-path to allow inlining of the fast-path.
    
     o.doSlow(f)
    
       }
    
    }
    
    
    
    func (o *Once) doSlow(f func()) {
    
       o.m.Lock()
    
       defer o.m.Unlock()
    
       if o.done == 0 {
    
          defer atomic.StoreUint32(&o.done, 1)
    
          f()
    
       }
    
    }
    

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论