Go语言中的单例模式

在过去的几年中,Go语言的发展是惊人的,并且吸引了很多由其他语言(Python、PHP、Ruby)转向Go语言的跨语言学习者。 Go语言太容易实现并发了,以至于它在很多地方被不正确的使用了。

Go语言中的单例模式

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

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

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

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

常见的错误

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

package singleton

type singleton struct {}

var instance *singleton

func GetInstance() *singleton {
if instance == nil {
instance = &singleton{} // 不是并发安全的
}
return instance
}

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

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

激进的加锁

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

var mu Sync.Mutex

func 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 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()
}
}

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

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

下面就是单例实现的完整代码,该实现利用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
}

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

结论

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

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

翻译自http://marcio.io/2015/07/singleton-pattern-in-go/,考虑到可读性部分内容有修改。

Go语言中的单例模式的更多相关文章

  1. Go语言中的单例模式(翻译)

    在过去的几年中,Go语言的发展是惊人的,并且吸引了很多由其他语言(Python.PHP.Ruby)转向Go语言的跨语言学习者. Go语言太容易实现并发了,以至于它在很多地方被不正确的使用了. Go语言 ...

  2. Objective-C中的单例模式

    ​    ​单例模式算是设计模式中比较简单的一种吧,设计模式不是只针对某种编程语言,在C++, Java, PHP等其他OOP语言也有设计模式,笔者初接触设计模式是通过<漫谈设计模式>了解 ...

  3. C# 中实现单例模式

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

  4. java中的单例模式与doublecheck

    转自: http://devbean.blog.51cto.com/448512/203501 在GoF的23种设计模式中,单例模式是比较简单的一种.然而,有时候越是简单的东西越容易出现问题.下面就单 ...

  5. python中的单例模式、元类

    单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场. ...

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

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

  7. Go语言如何实现单例模式

    单例模式是常见的设计模式,被广泛用于创建数据库,redis等单实例.作用在于可以控制实例个数节省系统资源 特点: 保证调用多次,只会产生单个实例 全局访问 单例的分类 单例模式大致分为2大类: 懒汉式 ...

  8. JAVA语言中的修饰符

    JAVA语言中的修饰符 -----------------------------------------------01--------------------------------------- ...

  9. Java语言中的面向对象特性总结

    Java语言中的面向对象特性 (总结得不错) [课前思考]  1. 什么是对象?什么是类?什么是包?什么是接口?什么是内部类?  2. 面向对象编程的特性有哪三个?它们各自又有哪些特性?  3. 你知 ...

随机推荐

  1. Effective Java 笔记

    1. 静态工厂 静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在.他的意思是面向接口编程??就是说我们只需知道接口,具体实现类是否存在没有关系?? 只提供静态工厂方法的主要 ...

  2. 一次错误使用 go-cache 导致出现的线上问题

    话说一个美滋滋的上午, 突然就出现大量报警, 接口大量请求都响应超时了. 排查过程 查看服务器的监控系统, CPU, 内存, 负载等指标正常 排查日志, 日志能够响应的结果也正常. request.l ...

  3. 机器人走方格-51nod解题

    M * N的方格,一个机器人从左上走到右下,只能向右或向下走. 有多少种不同的走法? 注意:给定 M, N 是一个正整数. 示例 输入: 1行, 2个数M,N,中间用空格隔开.(2 <= m,n ...

  4. IDEA关于-javaagent中文路径导致无法debug的初步解决方案

    IDEA明明是装在D盘的纯英文路径,而且java的相关环境变量全部是在英文路径,结果还是因为中文路径的问题导致无法debug.仔细观察,发现这个中文路径是作为java的-javaagent的参数输入的 ...

  5. 某大佬的TODOLIST

    回文串 manacher(完成时间:2018.12.10)回文串计数最长双回文串(完成时间:2018.12.10) 扫描线 棋盘制作巨大的牛棚玉蟾宫某个blog 汉诺塔相关 新汉诺塔SHOI 博弈论 ...

  6. postgresql高级应用之行转列&汇总求和

    postgresql高级应用之行转列&汇总求和 轉載請注名出處 https://www.cnblogs.com/funnyzpc/p/14732165.html 前言 节前公司业务方需要做一個 ...

  7. addslashes,htmlspecialchars,htmlentities转换或者转义php特殊字符防止xss攻击以及sql注入

    一.转义或者转换的目的 1. 转义或者转换字符串防止sql注入 2. 转义或者转换字符防止html非过滤引起页面布局变化 3. 转义或者转换可以阻止javascript等脚本的xss攻击,避免出现类似 ...

  8. 反病毒攻防研究第004篇:利用WinRAR与AutoRun.inf实现自启动

    一.前言 由之前的一系列研究可以发现,为了使得"病毒"能够实现自启动,我也是煞费苦心,采取了各种方式,往往需要编写冗长的代码并且还需要掌握系统底层或注册表的很多知识才可以.而这次我 ...

  9. hdu 5059 判断数字表示方式以及范围合法(int型之内)

    题意:       给你一个串和两个整数a,b,问你这个串表示的数字是否合法,并且在a,b之间, 和法的要求是无论是正数还是负数都没有前导0,并且注意 -0 是不合法的. 思路:       写了将近 ...

  10. Andrew Ng机器学习算法入门(一):简介

    简介 最近在参加一个利用机器学习来解决安全问题的算法比赛,但是对机器学习的算法一直不了解,所以先了解一下机器学习相关的算法. Andrew Ng就是前段时间从百度离职的吴恩达.关于吴恩达是谁,相信程序 ...