持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
🎐 放在前面说的话
大家好,我是北 👧🏻
本科在读,此为日常捣鼓.
如有不对,请多指教,也欢迎大家来跟我讨论鸭 👏👏👏
还有还有还有很重要的,麻烦大可爱们动动小手,给北点颗心心♥,北北需要点鼓励嗷呜~谢谢
今天是我们「Go并发」系列的第四篇:「sync包并发同步原语(2)」;
Let’s get it!
一、sync.Map
在这之前,我们先来浅谈一下原生Go Map,Go Map 在并发读写场景下,因为是非线程安全的,并发读写过程中map的数据容易被写乱,所以经常会遇到panic的情况。对付这种情况,我们的思路一般是map加锁,或将map分为若干个小map,对key进行哈希。原生map的处理方式要么锁的粒度比较大,影响效率;要么实现复杂,易出错。 基于此业界也引申出了map的两种目前在业界使用的最多的并发模式。
- 原生map + 互斥锁或读写互斥锁
- 标准库sync.Map(Go1.9以后)
下面我们主要讲一下
sync.Map的用法
1.sync.Map 概念
- 对map读写,无需加锁
- 通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。
- 线程安全的,添加、检索、删除都保持着常数级的时间复杂度
- 零值有效,且是一个空map。第一次使用后,不允许再拷贝
2.sync.Map方法
| 方法 | 功能 |
|---|---|
| Store(interface{},interface{}) | 添加元素 |
| Load(interface{},interface{}) | 检索元素 |
| Delete(interface{}) | 删除元素 |
| LoadOrStoreinterface{},interface{}) (interface{},bool) | 检索或添加之前不存在的元素。存在,则为true |
| Range | 遍历元素 |
sync.map 适用于读多写少的场景。
3. sync.Map栗子
func main() {
var m sync.Map
// 添加 写入两个key-values
m.Store("ZzZ959", 19)
m.Store("L", 22)
// 检索 读取其中的一个 key 并打印其age
age, _ := m.Load("ZzZ959")
fmt.Println(age.(int))
// 遍历 遍历所有key-values,并打印
m.Range(func(key, value interface{}) bool {
name := key.(string)
age := value.(int)
fmt.Println(name, age)
return true
})
// 删除 删除其中的一个 key,再读这个 key,得到的就是 nil
m.Delete("L")
age, ok := m.Load("L")
fmt.Println(age, ok)
// 检索 尝试读取或写入 "Anna",不存在,写入成功,并读出age。
m.LoadOrStore("Anna", 3)
age, _ = m.Load("Anna")
fmt.Println(age)
}
打印:
19
L 22
ZzZ959 19
nil false
3
二、sync.Pool
1.sync.Pool概念
形象理解sync.Pool
横幅签字仪式:现在在某个角落放一个箱子 ( 类比成 sync.Pool ) ,学生签字之后,笔就丢到箱子里,下一个学生要用笔的话,伸手进箱子摸一下,看下有笔吗?有的话,就拿来用了。没有的话,就再找人要一支新笔。这样新笔的使用数量就大大减少了,桌上也没有用过的杂七杂八的笔了,工作人员也轻松了。但前提是,保证箱子里每时每刻都有一支用过的笔
正经理解sync.Pool
在高并发场景下,我们会遇到很多问题,垃圾回收(GC)就是其中之一。Go 中的垃圾回收是自动执行的,有利有弊,频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。
- 并发池,负责安全地保存一组对象
- 自动扩容、缩容
- 本质:增加临时对象的重用率,减少 GC 负担
- 复用已经使用过的对象,来达到优化内存使用和回收的目的
2.sync.Pool方法
| 方法 | 功能 |
|---|---|
| New | 初始化 Pool |
| Get() interface{} | 申请一个元素 |
| Put(interface{}) | 释放一个元素 |
3. sync.Pool栗子
// 用来统计实例真正创建的次数
var num int32
// 创建实例的函数
func create() interface{} {
// 这里必须使用原子加,不然有并发问题
// num++
atomic.AddInt32(&num, 1)
buffer := make([]byte, 100)
return &buffer
}
func main() {
// 创建实例即初始化
Pool := &sync.Pool{
New: create,
}
// 并发测试
numWorkers := 100 * 1024
var wg sync.WaitGroup
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {
defer wg.Done()
// 申请一个 buffer 实例
buffer := Pool.Get()
_ = buffer.(*[]byte)
// 释放一个 buffer 实例
defer Pool.Put(buffer)
}()
}
wg.Wait()
fmt.Printf("%d buffers were created.\n", num)
}
打印:
7 buffers were created.
有可能不是7,这需要看每个goroutine的执行速度,快的话,实际创建的少,慢则反之
三、sync.Once
1. sync.Once概念
sync.Once是让函数方法只被调用执行一次的实现,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接等。在后面,我们会再出一篇关于模式设计的。
- 确保一个函数只执行一次
适用场景
- 全局变量初始化
- 懒汉模式的单例
- 服务接受系统级别的kill信号去触发业务代码
- 所有只允许执行一次的场景。
2. sync.Once方法
| 方法 | 功能 |
|---|---|
| Do(func ()) | 指定只能被调用一次的部分 |
3. sync.Once栗子
func main() {
var once sync.Once
var wg sync.WaitGroup
for i := 1; i < 10; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
// fmt.Println("once", index)
once.Do(func() {
fmt.Println("once", index)
})
}(i)
}
wg.Wait()
fmt.Printf("end...")
}
打印:
once 9
end...
如果不用的话,打印(换行):
once 9 once 4 once 1 once 2 once 3 once 5 once 6 once 8 once 7 end...
🎉 放在后面的话
本文我们介绍了 Go 语言中的基本同步原语sync.Map、sync.Pool、sync.Once的概念和简单应用,并对sync.Map和map进行了比较。 高并发下:sync.Map线程安全的,添加、检索、删除都保持着常数级的时间复杂度,且降低了锁时间来提高效率;sync.Pool 复用对象的内存,减轻 GC 的压力,提升系统的性能;sync.Once是经典懒汉模式的单例必选。