Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言sync锁与对象池

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

作者:onlooker6666

本文介绍了Go语言标准库sync包提供的并发控制工具,主要包括互斥锁(sync.Mutex)和读写锁(sync.RWMutex)两类同步机制,下面就来具体介绍一下这两个的使用,感兴趣的可以了解一下

1. 背景

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

2. 锁

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,前者是互斥锁,后者是读写锁。

2.1 互斥锁 (sync.Mutex)

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

2.1.1 使用方法

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

2.2 读写锁 (sync.RWMutex)

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

func (*RWMutex) Lock // 写锁定
func (*RWMutex) Unlock // 写解锁
func (*RWMutex) RLock // 读锁定
func (*RWMutex) RUnlock // 读解锁
操作1 \ 操作2RLock()(读锁)Lock()(写锁)RUnlock()Unlock()
RLock()✅ 并发允许❌ 阻塞等待写锁释放✅ 无影响✅ 无影响
Lock()❌ 阻塞等待读锁释放❌ 阻塞等待写锁释放✅ 无影响✅ 无影响
RUnlock()✅ 无影响✅ 无影响✅ 无影响✅ 无影响
Unlock()✅ 无影响✅ 无影响✅ 无影响✅ 无影响

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

3. 对象池 (sync.Pool)

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

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

3.1 使用方法

只需要实现 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)

3.2 底层解析

3.2.1 sync.Pool 数据结构

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.
}

3.2.2 sync.Pool 的核心方法

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
}

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()
}

3.2.2 对象池的回收

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

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

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

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

您可能感兴趣的文章:
阅读全文