Go语言sync锁与对象池的实现

Written by

in

文章目录
  • 在并发编程中,正确地管理共享资源是构建高性能程序的关键。Go 语言标准库中的 sync 包提供了一组基础而强大的并发原语,用于实现安全的协程间同步与资源控制。本文将简要介绍 sync 包中常用的类型和方法: sync 锁 与 对象池,帮助开发者更高效地编写并发安全的 Go 程序。
  • go语言是出了名的高并发利器 , 但在高并发场景下 , 伴随而来的数据安全问题是需要解决的。 加锁就是其中的一个解决办法。 多个线程同时访问临界区,锁住一些共享资源, 以防止并发访问这些共享数据时可能导致的数据不一致问题。 获取锁的线程可以正常访问临界区,未获取到锁的线程等待锁释放后可以尝试获取锁。 sync.Locker 是 go 标准库 sync 下定义的锁接口: // A Locker represents an object that can be locked and unlocked. type Locker interface { Lock() Unlock() } 任何实现了 Lock 和 Unlock 两个方法的类,都可以作为一种锁的实现。Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁。
  • sync.Pool 的使用场景 : 保存和复用临时对象,减少内存分配,降低 GC 压力。举例 : Gin 框架中的 context 包每次面对接口调用时都需要创建 ,贯穿整个调用链路。底层就使用对象池进行优化。 sync.Pool 是可伸缩的,同时也是并发安全的,其大小仅受限于内存的大小。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。
  • 目录
    • 1. 背景
    • 2. 锁
      • 2.1 互斥锁 (sync.Mutex)
        • 2.1.1 使用方法
      • 2.2 读写锁 (sync.RWMutex)
      • 3. 对象池 (sync.Pool)
        • 3.1 使用方法
          • 3.2 底层解析
            • 3.2.1 sync.Pool 数据结构
            • 3.2.2 sync.Pool 的核心方法

        在并发编程中,正确地管理共享资源是构建高性能程序的关键。Go 语言标准库中的 sync 包提供了一组基础而强大的并发原语,用于实现安全的协程间同步与资源控制。本文将简要介绍 sync 包中常用的类型和方法: sync 锁 与 对象池,帮助开发者更高效地编写并发安全的 Go 程序。

        go语言是出了名的高并发利器 , 但在高并发场景下 , 伴随而来的数据安全问题是需要解决的。 加锁就是其中的一个解决办法。

        多个线程同时访问临界区,锁住一些共享资源, 以防止并发访问这些共享数据时可能导致的数据不一致问题

        获取锁的线程可以正常访问临界区,未获取到锁的线程等待锁释放后可以尝试获取锁。

        sync.Locker 是 go 标准库 sync 下定义的锁接口:

        // A Locker represents an object that can be locked and unlocked.
        type Locker interface {
            Lock()
            Unlock()
        }
        

        任何实现了 Lock 和 Unlock 两个方法的类,都可以作为一种锁的实现。
        Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁。

        互斥即不可同时运行。即使用了互斥锁的两个代码片段互相排斥,只有其中一个代码片段执行完成后,另一个才能执行。
        Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

        • Lock 加锁
          使用 Lock () 加锁后,该线程不能再继续对其加锁,否则会 panic。只有在 unlock () 之后才能再次 Lock ()。异步调用 Lock (),是正当的锁竞争,当然不会有 panic 了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。

        • Unlock 释放锁
          Unlock () 用于解锁 m,如果在使用 Unlock () 前未加锁,就会引起一个运行错误。已经锁定的 Mutex 并不与特定的 goroutine 相关联,这样可以利用一个 goroutine 对其加锁,再利用其他 goroutine 对其解锁。

        var lck sync.Mutex
        func foo() {
            lck.Lock() 
            defer lck.Unlock()
            // ...
        }
        

        读写锁是分别针对读操作和写操作进行锁定和解锁操作的互斥锁。
        RWMutex 提供四个方法:

        func (*RWMutex) Lock // 写锁定
        func (*RWMutex) Unlock // 写解锁
        func (*RWMutex) RLock // 读锁定
        func (*RWMutex) RUnlock // 读解锁
        
        • 写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;
        • 读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞
        操作1 操作2 RLock()(读锁) Lock()(写锁) RUnlock() Unlock()
        RLock() ✅ 并发允许 ❌ 阻塞等待写锁释放 ✅ 无影响 ✅ 无影响
        Lock() ❌ 阻塞等待读锁释放 ❌ 阻塞等待写锁释放 ✅ 无影响 ✅ 无影响
        RUnlock() ✅ 无影响 ✅ 无影响 ✅ 无影响 ✅ 无影响
        Unlock() ✅ 无影响 ✅ 无影响 ✅ 无影响 ✅ 无影响

        读写锁的存在是为了解决读多写少时的性能问题,读场景较多时,读写锁可有效地减少锁阻塞的时间。

        sync.Pool 的使用场景 : 保存和复用临时对象,减少内存分配,降低 GC 压力。
        举例 : Gin 框架中的 context 包每次面对接口调用时都需要创建 ,贯穿整个调用链路。底层就使用对象池进行优化。

        sync.Pool 是可伸缩的,同时也是并发安全的,其大小仅受限于内存的大小。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。

        只需要实现 New 函数即可。对象池中没有对象时,将会调用 New 函数创建。

        初始化 :

        var studentPool = sync.Pool{
            New: func() interface{} { 
                return new(Student) 
            },
        }
        

        关键操作 :

        // Put adds x to the pool.
        func (p *Pool) Put(x any);
        
        // Get selects an arbitrary item from the [Pool], removes it from the
        // Pool, and returns it to the caller.
        // Get may choose to ignore the pool and treat it as empty.
        // Callers should not assume any relation between values passed to [Pool.Put] and
        // the values returned by Get.
        //
        // If Get would otherwise return nil and p.New is non-nil, Get returns
        // the result of calling p.New.
        func (p *Pool) Get() any;
        

        举例 :

        stu := studentPool.Get().(*Student)
        json.Unmarshal(buf, stu)
        studentPool.Put(stu)
        
        • Get() 用于从对象池中获取对象,因为返回值是 interface{},因此需要类型转换。
        • Put() 则是在对象使用完毕后,返回对象池。

        type Pool struct {
            noCopy noCopy
        
            local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
            localSize uintptr        // size of the local array
        
            victim     unsafe.Pointer // local from previous cycle
            victimSize uintptr        // size of victims array
        
            // New optionally specifies a function to generate
            // a value when Get would otherwise return nil.
            // It may not be changed concurrently with calls to Get.
            New func() 
            }
        

        • noCopy 防拷贝标志;

        • local 类型为 [P]poolLocal 的数组,数组容量 P 为 goroutine 处理器 P 的个数;

        • victim 为经过一轮 GC 回收,暂存的上一轮 local;类型于二级缓存 , 随时可能被GC 回收

        • New 为用户指定的工厂函数,当 Pool 内存量元素不足时,会调用该函数构造新的元素.

        [P]poolLocal 数组
        暂时存储对象的对象池 , 每个 poolLocal 逻辑处理器分为 privatesharedList 两部分缓存数据

        type poolLocal struct {
            poolLocalInternal
        }
        
        // Local per-P Pool appendix.
        type poolLocalInternal struct {
            private any       // Can be used only by the respective P.
            shared  poolChain // Local P can pushHead/popHead; any P can popTail.
        }
        
        • poolLocal 为 Pool 中对应于某个 P 的缓存数据;
        • poolLocalInternal.private:对应于某个 P 的私有元素,操作时无需加锁;
        • poolLocalInternal.shared: 某个 P 下的共享元素链表,由于各 P 都有可能访问,因此需要加锁.

        3.2.2.1 Pool.Get

        Get流程

        func (p *Pool) Get() any {
            l, pid := p.pin()
            x := l.private
            l.private = nil
            if x == nil {
                x, _ = l.shared.popHead()
                if x == nil {
                    x = p.getSlow(pid)
                }
            }
            runtime_procUnpin()
            if x == nil && p.New != nil {
                x = p.New()
            }
            return x
        }
        
        • 调用 Pool.pin 方法,绑定当前 goroutine 与 P,并且取得该 P 对应的缓存数据;
        • 尝试获取 P 缓存数据的私有元素 private;
        • 倘若前一步失败,则尝试取 P 缓存数据中共享元素链表的头元素;
        • 倘若前一步失败,则走入 Pool.getSlow 方法,尝试取其他 P 缓存数据中共享元素链表的尾元素;
        • 同样在 Pool.getSlow 方法中,倘若前一步失败,则尝试从上轮 gc 前缓存中取元素(victim);
        • 调用 native 方法解绑 当前 goroutine 与 P
        • 倘若(2)-(5)步均取值失败,调用用户的工厂方法,进行元素构造并返回.

        3.2.2.1 Pool.Put

        Put流程

        / Put adds x to the pool.
        func (p *Pool) Put(x any) {
            if x == nil {
                return
            }
            l, _ := p.pin()
            if l.private == nil {
                l.private = x
            } else {
                l.shared.pushHead(x)
            }
            runtime_procUnpin()
        }
        
        • 判断存入元素 x 非空;
        • 调用 Pool.pin 绑定当前 goroutine 与 P,并获取 P 的缓存数据;
        • 倘若 P 缓存数据中的私有元素为空,则将 x 置为其私有元素;
        • 倘若未走入(3)分支,则将 x 添加到 P 缓存数据共享链表的末尾;
        • 解绑当前 goroutine 与 P.

        3.2.2 对象池的回收

        存入 pool 的对象会不定期被 go 运行时回收,因此 pool 没有容量概念,即便大量存入元素,也不会发生内存泄露.

        具体回收时机是在 gc 时执行的:

        • 每个 Pool 首次执行 Get 方法时,会在内部首次调用 pinSlow 方法内将该 pool 添加到迁居的 allPools 数组中;
        • 每次 gc 时,会将上一轮的 oldPools 清空,并将本轮 allPools 的元素赋给 oldPools,allPools 置空;
        • 新置入 oldPools 的元素统一将 local 转移到 victim,并且将 local 置为空.

        综上可以得见,最多两轮 gc,pool 内的对象资源将会全被回收.

        到此这篇关于Go语言sync锁与对象池的实现的文章就介绍到这了,更多相关Go语言sync锁与对象池内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

        您可能感兴趣的文章:

        • Go多线程中数据不一致问题的解决方案(sync锁机制)
        • 一文详解Go语言中对象池的正确打开方式
        • 深入理解Go语言对象池
        • Go语言sync.Pool对象池使用场景基本示例

        站内搜索