转载地址:https://mp.weixin.qq.com/s/JAlt0JQt8hkPCxYbmOMFJw

Go语言中的单例模式

在过去的几年中,Go语言的发展是惊人的,并且吸引了很多由其他语言(Python、PHP、Ruby)转向Go语言的跨语言学习者。

在过去的很长时间里,很多开发人员和初创公司都习惯使用Python、PHP或Ruby快速开发功能强大的系统,并且大多数情况下都不需要担心内部事务如何工作,也不需要担心线程安全性和并发性。直到最近几年,多线程高并发的系统开始流行起来,我们现在不仅需要快速开发功能强大的系统,而且还要保证被开发的系统能够足够快速运行。(我们真是太难了

对于被Go语言天生支持并发的特性吸引来的跨语言学习者来说,我觉着掌握Go语言的语法并不是最难的,最难的是突破既有的思维定势,真正理解并发和使用并发来解决实际问题。

Go语言太容易实现并发了,以至于它在很多地方被不正确的使用了。

常见的错误

有一些错误是很常见的,比如不考虑并发安全的单例模式。就像下面的示例代码:

package singletontype singleton struct {}var instance *singletonfunc GetInstance() *singleton {if instance == nil {instance = &singleton{}   // 不是并发安全的}return instance
}

在上述情况下,多个goroutine可以执行第一个检查,并且它们都将创建该singleton类型的实例并相互覆盖。无法保证它将在此处返回哪个实例,并且对该实例的其他进一步操作可能与开发人员的期望不一致。

不好的原因是,如果有代码保留了对该单例实例的引用,则可能存在具有不同状态的该类型的多个实例,从而产生潜在的不同代码行为。这也成为调试过程中的一个噩梦,并且很难发现该错误,因为在调试时,由于运行时暂停而没有出现任何错误,这使非并发安全执行的可能性降到了最低,并且很容易隐藏开发人员的问题。

激进的加锁

也有很多对这种并发安全问题的糟糕解决方案。使用下面的代码确实能解决并发安全问题,但会带来其他潜在的严重问题,通过加锁把对该函数的并发调用变成了串行。

var mu Sync.Mutexfunc GetInstance() *singleton {mu.Lock()                    // 如果实例存在没有必要加锁defer mu.Unlock()if instance == nil {instance = &singleton{}}return instance
}

在上面的代码中,我们可以看到在创建单例实例之前通过引入Sync.Mutex和获取Lock来解决并发安全问题。问题是我们在这里执行了过多的锁定,即使我们不需要这样做,在实例已经创建的情况下,我们应该简单地返回缓存的单例实例。在高度并发的代码基础上,这可能会产生瓶颈,因为一次只有一个goroutine可以获得单例实例。

因此,这不是最佳方法。我们必须考虑其他解决方案。

Check-Lock-Check模式

在C ++和其他语言中,确保最小程度的锁定并且仍然是并发安全的最佳和最安全的方法是在获取锁定时利用众所周知的Check-Lock-Check模式。该模式的伪代码表示如下。

if check() {lock() {if check() {// 在这里执行加锁安全的代码}}
}

该模式背后的思想是,你应该首先进行检查,以最小化任何主动锁定,因为IF语句的开销要比加锁小。其次,我们希望等待并获取互斥锁,这样在同一时刻在那个块中只有一个执行。但是,在第一次检查和获取互斥锁之间,可能有其他goroutine获取了锁,因此,我们需要在锁的内部再次进行检查,以避免用另一个实例覆盖了实例。

如果将这种模式应用于我们的GetInstance()方法,我们会写出类似下面的代码:

func GetInstance() *singleton {if instance == nil {     // 不太完美 因为这里不是完全原子的mu.Lock()defer mu.Unlock()if instance == nil {instance = &singleton{}}}return instance
}

通过使用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
}

但是……这看起来有点繁琐了,我们其实可以通过研究Go语言和标准库如何实现goroutine同步来做得更好。

Go语言惯用的单例模式

我们希望利用Go惯用的方式来实现这个单例模式。我们在标准库sync中找到了Once类型。它能保证某个操作仅且只执行一次。下面是来自Go标准库的源码(部分注释有删改)。

// 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 uint32m    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()                          // lockdefer o.m.Unlock()if o.done == 0 {                    // checkdefer atomic.StoreUint32(&o.done, 1)f()}
}

这说明我们可以借助这个实现只执行一次某个函数/方法,once.Do()的用法如下:

once.Do(func() {// 在这里执行安全的初始化
})

下面就是单例实现的完整代码,该实现利用sync.Once类型去同步对GetInstance()的访问,并确保我们的类型仅被初始化一次。

package singletonimport ("sync"
)type singleton struct {}var instance *singleton
var once sync.Oncefunc GetInstance() *singleton {once.Do(func() {instance = &singleton{}})return instance
}

因此,使用sync.Once包是安全地实现此目标的首选方式,类似于Objective-C和Swift(Cocoa)实现dispatch_once方法来执行类似的初始化。

结论

当涉及到并发和并行代码时,需要对代码进行更仔细的检查。始终让你的团队成员执行代码审查,因为这样的事情很容易就会被发现。

所有刚转到Go语言的新开发人员都必须真正了解并发安全性如何工作以更好地改进其代码。即使Go语言本身通过允许你在对并发性知识知之甚少的情况下设计并发代码,也完成了许多繁重的工作。在某些情况下,单纯的依靠语言特性也无能为力,你仍然需要在开发代码时应用最佳实践。

翻译自http://marcio.io/2015/07/singleton-pattern-in-go/,考虑到可读性内容与原文略有差异。

Go语言中的单例模式

在过去的几年中,Go语言的发展是惊人的,并且吸引了很多由其他语言(Python、PHP、Ruby)转向Go语言的跨语言学习者。

在过去的很长时间里,很多开发人员和初创公司都习惯使用Python、PHP或Ruby快速开发功能强大的系统,并且大多数情况下都不需要担心内部事务如何工作,也不需要担心线程安全性和并发性。直到最近几年,多线程高并发的系统开始流行起来,我们现在不仅需要快速开发功能强大的系统,而且还要保证被开发的系统能够足够快速运行。(我们真是太难了

对于被Go语言天生支持并发的特性吸引来的跨语言学习者来说,我觉着掌握Go语言的语法并不是最难的,最难的是突破既有的思维定势,真正理解并发和使用并发来解决实际问题。

Go语言太容易实现并发了,以至于它在很多地方被不正确的使用了。

常见的错误

有一些错误是很常见的,比如不考虑并发安全的单例模式。就像下面的示例代码:

package singletontype singleton struct {}var instance *singletonfunc GetInstance() *singleton {if instance == nil {instance = &singleton{}   // 不是并发安全的}return instance
}

在上述情况下,多个goroutine可以执行第一个检查,并且它们都将创建该singleton类型的实例并相互覆盖。无法保证它将在此处返回哪个实例,并且对该实例的其他进一步操作可能与开发人员的期望不一致。

不好的原因是,如果有代码保留了对该单例实例的引用,则可能存在具有不同状态的该类型的多个实例,从而产生潜在的不同代码行为。这也成为调试过程中的一个噩梦,并且很难发现该错误,因为在调试时,由于运行时暂停而没有出现任何错误,这使非并发安全执行的可能性降到了最低,并且很容易隐藏开发人员的问题。

激进的加锁

也有很多对这种并发安全问题的糟糕解决方案。使用下面的代码确实能解决并发安全问题,但会带来其他潜在的严重问题,通过加锁把对该函数的并发调用变成了串行。

var mu Sync.Mutexfunc GetInstance() *singleton {mu.Lock()                    // 如果实例存在没有必要加锁defer mu.Unlock()if instance == nil {instance = &singleton{}}return instance
}

在上面的代码中,我们可以看到在创建单例实例之前通过引入Sync.Mutex和获取Lock来解决并发安全问题。问题是我们在这里执行了过多的锁定,即使我们不需要这样做,在实例已经创建的情况下,我们应该简单地返回缓存的单例实例。在高度并发的代码基础上,这可能会产生瓶颈,因为一次只有一个goroutine可以获得单例实例。

因此,这不是最佳方法。我们必须考虑其他解决方案。

Check-Lock-Check模式

在C ++和其他语言中,确保最小程度的锁定并且仍然是并发安全的最佳和最安全的方法是在获取锁定时利用众所周知的Check-Lock-Check模式。该模式的伪代码表示如下。

if check() {lock() {if check() {// 在这里执行加锁安全的代码}}
}

该模式背后的思想是,你应该首先进行检查,以最小化任何主动锁定,因为IF语句的开销要比加锁小。其次,我们希望等待并获取互斥锁,这样在同一时刻在那个块中只有一个执行。但是,在第一次检查和获取互斥锁之间,可能有其他goroutine获取了锁,因此,我们需要在锁的内部再次进行检查,以避免用另一个实例覆盖了实例。

如果将这种模式应用于我们的GetInstance()方法,我们会写出类似下面的代码:

func GetInstance() *singleton {if instance == nil {     // 不太完美 因为这里不是完全原子的mu.Lock()defer mu.Unlock()if instance == nil {instance = &singleton{}}}return instance
}

通过使用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
}

但是……这看起来有点繁琐了,我们其实可以通过研究Go语言和标准库如何实现goroutine同步来做得更好。

Go语言惯用的单例模式

我们希望利用Go惯用的方式来实现这个单例模式。我们在标准库sync中找到了Once类型。它能保证某个操作仅且只执行一次。下面是来自Go标准库的源码(部分注释有删改)。

// 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 uint32m    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()                          // lockdefer o.m.Unlock()if o.done == 0 {                    // checkdefer atomic.StoreUint32(&o.done, 1)f()}
}

这说明我们可以借助这个实现只执行一次某个函数/方法,once.Do()的用法如下:

once.Do(func() {// 在这里执行安全的初始化
})

下面就是单例实现的完整代码,该实现利用sync.Once类型去同步对GetInstance()的访问,并确保我们的类型仅被初始化一次。

package singletonimport ("sync"
)type singleton struct {}var instance *singleton
var once sync.Oncefunc GetInstance() *singleton {once.Do(func() {instance = &singleton{}})return instance
}

因此,使用sync.Once包是安全地实现此目标的首选方式,类似于Objective-C和Swift(Cocoa)实现dispatch_once方法来执行类似的初始化。

结论

当涉及到并发和并行代码时,需要对代码进行更仔细的检查。始终让你的团队成员执行代码审查,因为这样的事情很容易就会被发现。

所有刚转到Go语言的新开发人员都必须真正了解并发安全性如何工作以更好地改进其代码。即使Go语言本身通过允许你在对并发性知识知之甚少的情况下设计并发代码,也完成了许多繁重的工作。在某些情况下,单纯的依靠语言特性也无能为力,你仍然需要在开发代码时应用最佳实践。

翻译自http://marcio.io/2015/07/singleton-pattern-in-go/,考虑到可读性内容与原文略有差异。

Go语言中的单例模式相关推荐

  1. c语言中如何确保一个程序是单例的_c++单例模式

    前言 在设计模式中,单例模式是最简单的一种.如何确保让一个资源在使用中只能实例化一次呢?如何确保在多线程环境下是线程安全的呢?本文将从最简单的单例到线程安全的单例一一讲解. 一.单线程环境 以下是最起 ...

  2. 【C++】C/C++ 中的单例模式

    目录 part 0:单例模式3种经典的实现方式 Meyer's Singleton Meyers Singleton版本二 Lazy Singleton Eager Singleton Testing ...

  3. C# 中实现单例模式

    文章目录 简介 不安全线程的单例模式 简单安全线程带锁 双重检查 - 带锁 安全初始化 安全并且懒汉式静态初始化 带泛型的懒汉式单例 异常 提高效率 总结 简介 单例模式是软件工程中广为人知的设计模式 ...

  4. 在Java中实现单例模式的有效方法是什么? [关闭]

    在Java中实现单例模式的有效方法是什么? #1楼 我使用Spring框架来管理我的单身人士. 它不会强制类的"单一性"(如果涉及多个类加载器,您将无法真正做到),但是它提供了一种 ...

  5. php应用数据库连接中的单例模式

    所谓的单例模式简而言之就是某个类在运行过程中只有一个实例,并且能够自行实例化并为整个系统的运行提供这个实例.在数据库连接中使用单例模式实例化数据库连接对象主要是可以避免重复的实例化对象而造成资源的浪费 ...

  6. python中的单例模式_Python单例模式

    Singleton(单例) -- 对象创建型模式 在GoF的<设计模式:可复用面向对象软件的基础>如下定义: 意图 保证一个类仅有一个实例,并且提供一个访问它的全局访问点. 适用性 在下面 ...

  7. android中的单例模式,Android中的单例模式

    定义: 单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 使用场景: 确保某一个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一 ...

  8. c语言随机字符rand,C语言中生产随机数 rand()函数

    一:如果你只要产生随机数而不需要设定范围的话,你只要用rand()就可以了:rand()会返回一随机数值, 范围在0至RAND_MAX 间.RAND_MAX定义在stdlib.h, 其值为214748 ...

  9. Go 知识点(19)— Go 语言中的野指针

    野指针是一种指向内存位置是不可知的指针,一般是由于指针变量在声明时没有初始化所导致的.在 Go语言中,布尔类型的零值为 false,数值类型的零值为 0,字符串类型的零值为 "", ...

最新文章

  1. pytorch笔记:torch.nn.GRU torch.nn.LSTM
  2. 剑指offer之两个队列实现栈的问题
  3. ffmpeg 缩放算法_图像尺寸调整算法介绍并手动实现近邻算法
  4. 传说之下地图素材_【开阔眼界】地图上的史记——名著轻松读
  5. C#DNS域名解析工具(DnsLookup)
  6. fopen与读写的标识r,r+,rb+,rt+,w+.....
  7. Introduction to Microservices
  8. Tomcat catalina.properties配置文件详解
  9. java 存储过程返回结果集_通过SQL“select * from”从java存储过程返回结果集
  10. 《设计模式之禅》之——六大设计原则解读
  11. Netty源码分析第3章(客户端接入流程)----第3节: NioSocketChannel的创建
  12. 论文笔记_S2D.55_2019_SLAM综述_Huang B. A Survey of Simultaneous Localization and Mapping
  13. 写了一个很矬( cuó)的移动端用户管理
  14. 基于PHP的在线聊天室(网页版)
  15. 如何使用Aspose.pdf读取 增值税发票pdf文件内容 和 解二维码
  16. 数据分析~中国五大城市PM2.5数据分析01
  17. 大道至简(周爱民)第二章-----读后感
  18. 万字详解 阿里面试真题:请你说说索引的原理
  19. 学计算机高中应选什么科目,学计算机高中需要选哪三科?高中自选三科怎么上课?...
  20. 计算机开机后无法网络拨号怎样处理,电脑不能上网了怎么办?教你宽带故障排查方法...

热门文章

  1. 制作一个状态栏中跑马灯效果_图标设计指南(3)——制作一个图标集所需全部信息(中)...
  2. html设置下拉筛选可以多选,select下拉框(支持筛选、多选)
  3. java匿名内部类_java中的匿名内部类
  4. xss绕过字符过滤_XSS绕过实战练习
  5. centos7--shell脚本自动实现bond配置-第二版
  6. 学习ActiveMQ(五):activemq的五种消息类型和三种监听器类型
  7. 以数据为中心的存储观
  8. 数据库事物用法 SET XACT_ABORT ON
  9. [javaSE] 网络编程(浏览器客户端-自定义服务端)
  10. linux下移除环境变量及mkdir命令介绍