写点什么

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:397214

评论

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

Web3 游戏周报(8.18 - 8.24)

Footprint Analytics

链游

Manulife IM发布自然资本、TCFD和SRI报告

财见

QCN9074, QCN9274, QCN9224 chip all-round analysis - leading the future of wireless networks

wifi6-yiyi

WiFi7

释放生产力:JNPF低代码开发平台的五大优势

不在线第一只蜗牛

低代码 数字化

ICEAI持续稳步发展,拓展全球市场交易版图

科技热闻

你敢在大学本科期间创办一家属于自己的公司吗?WTEAM 2024 年度大会,广州,9.10丨伙伴活动推荐

声网

人社大赛算法赛题解题思路分享+第五名

阿里云天池

#大数据

第三期安全AI挑战者计划-文本分类对抗攻击 第十名「你钉起来真好听」技术总结

阿里云天池

袋鼠云产品功能更新报告11期|能力AI+,实力拿捏!

袋鼠云数栈

低代码革命:JNPF平台如何简化企业应用开发

EquatorCoco

低代码

强强联手!媒体行业正式启用“算力包”模式算力服务!

九章云极DataCanvas

智能弹性实践:Kubernetes HPA 与观测云的自定义指标集成

观测云

k8s

使用poetry来更优雅的管理 python 包

我再BUG界嘎嘎乱杀

Python 编程 后端 开发语言 Poetry

什么是算力?

九章云极DataCanvas

心辰 Lingo 语音 AI 模型开放内测预约;Meta Sapiens 模型让 AI 分析图像中人类动作丨 RTE 开发者日报

声网

AutoGPT理念与应用

霍格沃兹测试开发学社

《2024快应用智慧服务生态白皮书》发布,探索AI与快应用融合之路

科技热闻

淘宝商品详情数据的高效获取与多元应用

Noah

商品详情API返回值中的商品重量与体积信息

技术冰糖葫芦

API Explorer API 测试 API 策略 pinduoduo API

奥运项目背后的智能制造世界:如何助力赛场的每一次超越?

Altair RapidMiner

人工智能 机器学习 数据分析 仿真 altair

极石汽车:找准细分市场定位,加速完成品牌进阶

Geek_2d6073

共筑运维稳定磐石!华为云“数智先锋100·企业运维稳定性治理研讨会”南京站成功举办

科技热闻

赛题解析 | 初赛赛道三:服务网格控制面分治体系构建

阿里云天池

云原生

2021全国数字生态创新大赛-智能算法赛季军方案 乘风破浪的炼丹师队

阿里云天池

优化系统性能:深入探讨Web层缓存与Redis应用的挑战与对策

不在线第一只蜗牛

redis 缓存 Web

【首席战略官分享】工作数字化的中国历程 | 从 OA 到 BPM 到数字流程自动化

望繁信科技

数字化转型 流程挖掘 流程智能

机器学习算法常用指标总结

阿里云天池

如何实现持续、主动、长效的数据治理?主动元数据或是最佳答案

Aloudata

数据治理 元数据 全链路数据血缘 数据血缘 数据链路

关于 Git 的6大提示和技巧

秃头小帅oi

淘宝拍立淘接口全攻略:轻松实现图片搜索商品

tbapi

淘宝API 淘宝图片搜索接口 淘宝拍立淘接口

聚道云软件连接器8月新增应用合集(3)

聚道云软件连接器

功能更新

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