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

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

第一个版本

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

go 复制代码
// 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

}

问题

  1. 指令重排序

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

  1. golang赋值不保证原子性

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

解决方法

读写锁

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

go 复制代码
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指针被完整赋值以后,才能被读到

go 复制代码
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,这里敲下黑板

go 复制代码
type Singleton struct{}



var (

   instancev3 *Singleton

   once       sync.Once

)



func GetInstancev3() *Singleton {

   once.Do(func() {

      instancev3 = &Singleton{}

   })

   return instancev3

}

性能比较

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

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

arduino 复制代码
// 读写锁

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的内联优化了函数调用的开销。

scss 复制代码
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()

   }

}
相关推荐
程序员爱钓鱼31 分钟前
Go语言统计字符串中每个字符出现的次数 — 简易频率分析器
后端·google·go
nextera-void1 天前
深入浅出 Golang:一次精神之旅
开发语言·golang·go
DemonAvenger1 天前
Go语言实现DNS解析与域名服务的实践与优化
网络协议·架构·go
痴人说梦梦中人1 天前
Gin框架统一响应与中间件机制学习笔记
网络安全·中间件·go·gin
程序员爱钓鱼1 天前
Go语言实战案例-判断回文字符串-是不是正着念反着念都一样?
后端·google·go
岁忧2 天前
(LeetCode 面试经典 150 题 ) 209. 长度最小的子数组(双指针)
java·c++·算法·leetcode·面试·go
熬了夜的程序员2 天前
【华为机试】HJ30 字符串合并处理
算法·华为·面试·go
一条GO2 天前
易犯的五个Go编码错误
go
Code季风2 天前
Go并发详解
go·编程语言·设计
程序员爱钓鱼2 天前
Go语言实战案例-字符串反转
后端·google·go