写点什么

Golang 标准库探秘(一):sync 标准库

  • 2016-02-28
  • 本文字数:2728 字

    阅读完需:约 9 分钟

编者按:号称”21 世纪的 c 语言“的 Golang 逐渐被越来越多的公司关注和使用,而 Golang 标准库则是编写 Golang 语言程序代码的基础,本文就将通过案例来讲解 sync 这个标准库。

在高并发或者海量数据的生产环境中,我们会遇到很多问题,GC(garbage collection,中文译成垃圾回收)就是其中之一。说起优化 GC 我们首先想到的肯定是让对象可重用,这就需要一个对象池来存储待回收对象,等待下次重用,从而减少对象产生数量。

标准库原生的对象池

在 Golang1.3 版本便已新增了 sync.Pool 功能,它就是用来保存和复用临时对象,以减少内存分配,降低 CG 压力,下面就来讲讲 sync.Pool 的基本用法。

复制代码
type Pool struct {
local unsafe.Pointer
localSize uintptr
New func() interface{}
}

很简洁,最常用的两个函数 Get/Put

复制代码
var pool = &sync.Pool{New:func()interface{}{return NewObject()}}
pool.Put()
Pool.Get()

对象池在 Get 的时候没有里面没有对象会返回 nil,所以我们需要 New function 来确保当获取对象对象池为空时,重新生成一个对象返回

复制代码
if p.New != nil {
return p.New()
}

在实现过程中还要特别注意的是 Pool 本身也是一个对象,要把 Pool 对象在程序开始的时候初始化为全局唯一。
对象池使用是较简单的,但原生的 sync.Pool 有个较大的问题:我们不能自由控制 Pool 中元素的数量,放进 Pool 中的对象每次 GC 发生时都会被清理掉。这使得 sync.Pool 做简单的对象池还可以,但做连接池就有点心有余而力不足了,比如:在高并发的情景下一旦 Pool 中的连接被 GC 清理掉,那每次连接 DB 都需要重新三次握手建立连接,这个代价就较大了。

既然存在问题,那我们就自行构建一个对象池吧。

对象池底层数据结构

我们选择用 Golang 的 container 标准包中的链表来做对象池的底层数据结构,它被封装在 container/list 标准包里:

复制代码
type Element struct {
next, prev *Element
list *List
Value interface{}
}

这里是定义了链表中的元素,这个标准库实现的是一个双向链表,并且已经为我们封装好了各种 Front/Back 方法。不过 Front 方法的实现和我们需要的还是有点差异,它只是返回链表中的第一个元素,但这个元素依然会链接在链表里,所以我们需要自行将它从链表中删除,remove 方法如下:

复制代码
func (l *List) remove(e *Element) *Element {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil
e.prev = nil
e.list = nil
l.len--
return e
}

这样对象池的核心部分就完成了,但注意一下,从 remove 函数可以看出,container/list 并不是线程安全的,所以在对象池的对象个数统计等一些功能会有问题。

原子操作并发安全

下面我们来自行解决并发安全的问题。Golang 的 sync 标准包封装了常用的原子操作和锁操作。
sync/atomic 封装了常用的原子操作。所谓原子操作就是在针对某个值进行操作的整个过程中,为了实现严谨性必须由一个独立的 CPU 指令完成,该过程不能被其他操作中断,以保证该操作的并发安全性。

复制代码
`type ConnPool struct {
conns []*conn
mu sync.Mutex // lock protected
len int32
}`

在 Golang 中,我们常用的数据类型除了 channel 之外都不是线程安全的,所以在这里我们需要对数量(len)和切片(conns []*conn)做并发保护。至于需要几把锁做保护,取决于实际场景,合理控制锁的粒度。
接着介绍一下锁操作,我们在 Golang 中常用的锁——互斥锁(Lock)和读写锁(RWLock),互斥锁和读写锁的区别是:互斥锁无论是读操作还是写操作都会对目标加锁也就是说所有的操作都需要排队进行,读写锁是加锁后写操作只能排队进行但是可以并发进行读操作,要注意一点就是读的时候写操作是阻塞的,写操作进行的时候读操作是阻塞的。类型 sync.Mutex/sync.RWMutex 的零值表示了未被锁定的互斥量。也就是说,它是一个开箱即用的工具。只需对它进行简单声明就可以正常使用了,例如(在这里以 Mutex 为例,相对于 RWMutex 也是同理):

复制代码
var mu sync.Mutex
mu.Lock()
mu.Unlock()

锁操作一定要成对出现,也就是说在加锁之后操作的某一个地方一定要记得释放锁,否则再次加锁会造成死锁问题

复制代码
fatal error: all goroutines are asleep - deadlock

不过在 Golang 里这种错误发生的几率会很少,因为有 defer 延时函数的存在
上面的代码可以改写为

复制代码
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()

在加锁之后马上用 defer 函数进行解锁操作,这样即使下面我们只关心函数逻辑而在函数退出的时候忘记 Unlock 操作也不会造成死锁,因为在函数退出的时候会自动执行 defer 延时函数释放锁。

标准库中的并发控制 -WaitGroup

sync 标准包还封装了其他很有用的功能,比如 WaitGroup,它能够一直等到所有的 goroutine 执行完成,并且阻塞主线程的执行,直到所有的 goroutine(Golang 中并发执行的协程)执行完成。文章开始我们说过,Golang 是支持并发的语言,在其他 goroutine 异步运行的时候主协程并不知道其他协程是否运行结束,一旦主协程退出那所有的协程就会退出,这时我们需要控制主协程退出的时间,常用的方法:

1、time.Sleep()

让主协程睡一会,好方法,但是睡多久呢?不确定(最简单暴力)

2、channel

在主协程一直阻塞等待一个退出信号,在其他协程完成任务后给主协程发送一个信号,主协程收到这个信号后退出

复制代码
e := make(chan bool)
go func() {
fmt.Println("hello")
e <- true
}()
<-e

3、waitgroup

给一个类似队列似得东西初始化一个任务数量,完成一个减少一个

复制代码
var wg sync.WaitGroup
func main() {
wg.Add(1)
go func() {
fmt.Println("hello")
wg.Done() // 完成
}()
wg.Wait()
}

这里要特别主要一下,如果 waitGroup 的 add 数量最终无法变成 0,会造成死锁,比如上面例子我 add(2) 但是我自始至终只有一个 Done, 那剩下的任务一直存在于 wg 队列中,主协程会认为还有任务没有完成便会一直处于阻塞 Wait 状态,造成死锁。
wg.Done 方法其实在底层调用的也是 wg.Add 方法,只是 Add 的是 -1

复制代码
func (wg *WaitGroup) Done() {
wg.Add(-1)
}

我们看 sync.WaitGroup 的 Add 方法源码可以发现,底层的加减操作用的是我们上面提到的 sync.atomic 标准包来确保原子操作,所以 sync.WaitGroup 是并发安全的。

作者简介

郭军,奇虎 360 安全卫士服务端技术团队成员,关注架构设计,GO 语言等互联网技术。


感谢姚梦龙对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-02-28 16:397701

评论

发布
暂无评论
发现更多内容

惊喜!1行Python代码,瞬间测你工作量,分享一个统计代码行数的神器

程序员晚枫

Python GitHub 开源项目 工作量

平台工程动态 MonthlyNews 2023-7

杨振涛

云原生 研发效能 平台工程 开发者体验 内部开发者平台

已确认!将数据安全管理纳入操作风险管理范畴

原点安全

【我和openGauss的故事】openGauss的WDR报告解读

daydayup

openGauss获2022年度创新产品奖!

daydayup

openGauss运维能力之SQLPatch解密

daydayup

Sprint Boot学习路线2

小万哥

Java spring Spring Cloud Spring Boot 后端

中文多模态医学大模型智能分析X光片,实现影像诊断,完成医生问诊多轮对话

汀丶人工智能

人工智能 自然语言处理 计算机视觉 多模态大模型

openGauss内核分析(三):SQL解析

daydayup

配置 Sublime Text4为 C++ 编辑器的方法

芯动大师

远程控制软件安全吗?一文看懂ToDesk、RayLink、TeamViewer、Splashtop相关安全机制

颜淡慕潇

安全 远程控制 远程桌面工具 ToDesk

什么是MES,什么是WMS,MES与WMS有什么区别?

优秀

MES系统 WMS系统

【我和openGauss的故事】openGauss易知易会的几个实用特性

daydayup

openGauss+Wasm:构建安全高效的UDF执行引擎

daydayup

北岩律师事务所:50000余家客户的选择,全国首家专注大消费领域的精品律所

联营汇聚

提升数据质量的四大有效方式

树上有只程序猿

数字化 数据质量

openGauss内核分析(四):查询重写(二)

daydayup

PoseiSwap:通过 RWA 的全新叙事,反哺 Nautilus Chain 生态

股市老人

数字人第一剑,先斩“尹天仇”

脑极体

AI

Apache IoTDB v1.1.1/v1.1.2 发布|增加 ZSTD 压缩,show variables 增加时间精度

Apache IoTDB

IoTDB Apache IoTDB

2023-07-31:用r、e、d三种字符,拼出一个回文子串数量等于x的字符串。 1 <= x <= 10^5。 来自百度。

福大大架构师每日一题

福大大架构师每日一题

大会议程正式公布 华为开发者大会看点前瞻

极客天地

杭州快盈施佳:基于支付宝小程序云的文旅数智新实践

TRaaS

小程序

最全的3D动画软件介绍来了!良心总结9款3D动画制作必备软件

龙智—DevSecOps解决方案

Unity 虚幻引擎 maya 3D动画软件 Houdini

DHorse v1.3.0 发布,基于k8s的发布平台

tiandizhiguai

DevOps k8s

IoTOS-App v0.6.0 智能诊断、用量/会话记录、批量业务办理、

开源物联卡管理平台-设备管理

物联网平台 IoT 开源软件 国产开源 物联网展

ChatPaper全流程加速科研:论文阅读+润色+优缺点分析与改进建议+审稿回复

汀丶人工智能

人工智能 自然语言处理 nlp chatpaper

亚马逊云科技如何助力中国企业建立“出海”的云端母港?

Lily

祝贺!openGauss社区技术委员会主席李国良当选2023 IEEE FELLOW

daydayup

Squids openGauss | 云上免费openGauss数据库服务

daydayup

Golang标准库探秘(一):sync 标准库_语言 & 开发_郭军_InfoQ精选文章