【Go并发】— sync包并发同步原语(2)

1,670 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

🎐 放在前面说的话

大家好,我是北 👧🏻

本科在读,此为日常捣鼓.

如有不对,请多指教,也欢迎大家来跟我讨论鸭 👏👏👏

还有还有还有很重要的,麻烦大可爱们动动小手,给北点颗心心♥,北北需要点鼓励嗷呜~谢谢

今天是我们「Go并发」系列的第四篇:「sync包并发同步原语(2)」;

u=4044355992,2659238210&fm=253&fmt=auto&app=138&f=PNG.webp

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是经典懒汉模式的单例必选。