Golang学习篇——对象池sync.Pool

目录

1. 什么是sync.Pool?

2. 为什么需要sync.Pool?

3. sync.Pool使用

4. sync.Pool源码分析

5. QA


1. 什么是sync.Pool?

Go 1.3 的sync包中加入一个新特性:Pool,官方文档

简单的说:它就是一个临时对象池,这个类设计的目的是用来保存和复用临时对象,以减少内存分配,降低CG压力。

2. 为什么需要sync.Pool?

增加临时对象的重用率,减少内存分配,减少GC负担,goroutine对象越多GC越慢,因为Golang进行三色标记回收的时候,要标记的也越多,自然就慢。

3. sync.Pool使用

思路:搞一个池子,预先放入临时产生的对象,然后取出使用

官方fmt包就是使用了sync.pool,由于fmt总是需要很多[]byte对象,索性就直接建了一个[]byte对象的池子。

package main

import (
	"fmt"
	"sync"
)

func main() {
	// 初始化一个pool
	pool := &sync.Pool{
		// 默认的返回值设置,不写这个参数,默认是nil
		New: func() interface{} {
			return 0
		},
	}

	// 看一下初始的值,这里是返回0,如果不设置New函数,默认返回nil
	init := pool.Get()
	fmt.Println(init)

	// 设置一个参数1
	pool.Put(1)

	// 获取查看结果
	num := pool.Get()
	fmt.Println(num)

	// 再次获取,会发现,已经是空的了,只能返回默认的值。
	num = pool.Get()
	fmt.Println(num)
}

4. sync.Pool源码分析

1) Pool结构分析

type Pool struct {
	// noCopy,防止当前类型被copy,是一个有意思的字段,后文详说。
	noCopy noCopy

    // [P]poolLocal 数组指针
	local     unsafe.Pointer
	// 数组大小
	localSize uintptr        

	// 选填的自定义函数,缓冲池无数据的时候会调用,不设置默认返回nil
	New func() interface{} //新建对象函数
}

type poolLocalInternal struct {
    // 私有缓存区
	private interface{}   
	// 公共缓存区
	shared  []interface{} 
	// 锁
	Mutex               
}

type poolLocal struct {
	// 每个P对应的pool
	poolLocalInternal

	// 这个字段很有意思,是为了防止“false sharing/伪共享”,后文详讲。
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

2) 基础函数pin,目的:确定当前P(调度器)绑定的localPool对象

流程:禁止抢占GC =》寻找偏移量=》检查越界 =》返回poolLocal=》加锁重建pool,并添加到allPool

func (p *Pool) pin() *poolLocal {
	// 返回当前 P.id && 设置禁止抢占(避免GC)
	pid := runtime_procPin()
	
	// 根据locaSize来获取当前指针偏移的位置
	s := atomic.LoadUintptr(&p.localSize) 
	l := p.local         
	
	// 有可能在运行中动调调整P,所以这里进行需要判断是否越界
	if uintptr(pid) < s {
	    // 没越界,直接返回
		return indexLocal(l, pid)
	}
	
    // 越界时,会涉及全局加锁,重新分配poolLocal,添加到全局列表
	return p.pinSlow()
}

var (
	allPoolsMu Mutex
	allPools   []*Pool
)


func (p *Pool) pinSlow() *poolLocal {
	// 取消P的禁止抢占(因为后面要进行metux加锁)
	runtime_procUnpin()
	
	// 加锁
	allPoolsMu.Lock()
	defer allPoolsMu.Unlock()
	
	// 返回当前 P.id && 设置禁止抢占(避免GC)
	pid := runtime_procPin()
	
	// 再次检查是否符合条件,有可能中途已被其他线程调用
	s := p.localSize
	l := p.local
	if uintptr(pid) < s {
		return indexLocal(l, pid)
	}
	
	// 如果数组为空,则新建Pool,将其添加到 allPools,GC以此获取所有 Pool 实例
	if p.local == nil {
		allPools = append(allPools, p)
	}
	
    // 根据 P 数量创建 slice
	size := runtime.GOMAXPROCS(0)
	local := make([]poolLocal, size)
	
	 // 将底层数组起始指针保存到 Pool.local,并设置 P.localSize
	 // 这里需要关注的是:如果GOMAXPROCS在GC间发生变化,则会重新分配的时候,直接丢弃老的,等待GC回收。
	atomic.StorePointer(&p.local, unsafe.Pointer(&local[0]))
	atomic.StoreUintptr(&p.localSize, uintptr(size))         
	
	// 返回本次所需的 poolLocal
	return &local[pid]
}

// 根据数据结构的大小来计算指针的偏移量
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
	lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
	return (*poolLocal)(lp)
}

3) put (优先放入private空间,后面再放入shared空间)

func (p *Pool) Put(x interface{}) {
	if x == nil {
		return
	}
    
    // 这段代码,不需要关心,降低竞争的
	if race.Enabled {
		if fastrand()%4 == 0 {
			// Randomly drop x on floor.
			return
		}
		race.ReleaseMerge(poolRaceAddr(x))
		race.Disable()
	}

    // 获取当前的poolLocal
	l := p.pin()

    // 如果private为nil,则优先进行设置,并标记x
	if l.private == nil {
		l.private = x
		x = nil
	}
	runtime_procUnpin()

    // 如果标记x不为nil,则将x设置到shared中
	if x != nil {
		l.Lock()
		l.shared = append(l.shared, x)
		l.Unlock()
	}
    
    // 设置竞争可用了。
	if race.Enabled {
		race.Enable()
	}
}

4) get

优先从private空间拿,不存在再继续加锁从shared空间拿,还没有再从其他的PoolLocal的shared空间拿,还没有就直接new一个返回。

func (p *Pool) Get() interface{} {
    // 竞争相关的设置
	if race.Enabled {
		race.Disable()
	}
    
    // 获取当前的poolLocal
	l := p.pin()

    // 从private中获取
	x := l.private
	l.private = nil
	runtime_procUnpin()

    // 不存在,则继续从shared空间拿,
	if x == nil {
	    // 加锁了,防止并发 
		l.Lock()
		last := len(l.shared) - 1
		if last >= 0 {
			x = l.shared[last]
            // 从尾巴开始拿起
			l.shared = l.shared[:last]
		}
		l.Unlock()
		if x == nil {
		    // 从其他的poolLocal中的shared空间看看有没有可返回的。
			x = p.getSlow()
		}
	}
    
    // 竞争解除
	if race.Enabled {
		race.Enable()
		if x != nil {
			race.Acquire(poolRaceAddr(x))
		}
	}
    
    // 如果还是没有的话,就直接new一个了
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

func (p *Pool) getSlow() (x interface{}) {
    // 获取poolLocal数组的大小
	size := atomic.LoadUintptr(&p.localSize) // load-acquire
	local := p.local                         // load-consume
	
	// 尝试从其他procs获取一个P对象
	pid := runtime_procPin()
	runtime_procUnpin()
	
	for i := 0; i < int(size); i++ {
        // 获取一个poolLocal,注意这里是从当前的local的位置开始获取的,目的是防止取到自身
		l := indexLocal(local, (pid+i+1)%int(size))
		// 加锁从尾部获取shared的数据
		l.Lock()
		last := len(l.shared) - 1
        // 若长度大于1
		if last >= 0 {
			x = l.shared[last]
			l.shared = l.shared[:last]
			l.Unlock()
			break
		}
		l.Unlock()
	}
	return x
}

5. QA

1) pool的是永久保存的吗?

会进行清理的,时间就是两次GC间隔的时间。 sync.Pool不适合放做“数据库连接池”等带持久性质的数据,因为它会定期回收。

2) 为什么获取shared要加锁,而private不用?

golang是MPG的方式运行的,每个P都分配一个localPool,在同一个P下面只会有一个Gouroutine在跑,所以这里的private,在同一时间就只可能被一个Gouroutine获取到。而shared就不一样了,有可能被其他的P给获取走,在同一时间就只可能被多个Gouroutine获取到,为了保证数据竞争,必须加一个锁来保证只会被一个G拿走。

3) noCopy的作用?

防止Pool被拷贝,因为Pool 在Golang是全局唯一。如何实现被防止拷贝:只要包含实现 sync.Locker 这个接口的结构体noCopy,go vet 就可以帮我们进行检查是否被拷贝。

4) pad的作用?

主要就是用来防止“伪共享”的。

5) 如何保证数据存储在LocalPool数组对应的单元?

根据数据结构的大小来计算指针的偏移量,进而算出是LocalPool数组的哪个。

参考链接

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
sync.Map 是 Go 语言标准库中提供的一种并发安全的字典类型,它可以被多个 goroutine 安全地访问和修改。在多个 goroutine 中并发地读写一个 map 时,会出现竞争条件,从而导致数据不一致。而 sync.Map 利用了一些锁的技巧,避免了这种竞争条件的发生,从而实现了高效的并发安全访问。 sync.Map 的 API 非常简单,主要包括以下几个方法: 1. Store(key, value interface{}):将一个键值对存储到 sync.Map 中。 2. Load(key interface{}) (value interface{}, ok bool):根据键从 sync.Map 中获取对应的值。 3. LoadOrStore(key, value interface{}) (actual interface{}, loaded bool):如果键存在于 sync.Map 中,则返回对应的值和 true,否则将键值对存储到 sync.Map 中并返回新的值和 false。 4. Delete(key interface{}):从 sync.Map 中删除一个键值对。 5. Range(f func(key, value interface{}) bool):遍历 sync.Map 中的键值对,并对每一个键值对调用函数 f,如果 f 返回 false,则停止遍历。 下面是一个使用 sync.Map 的简单例子,展示了如何在多个 goroutine 中并发地访问和修改 sync.Map: ``` package main import ( "fmt" "sync" ) func main() { var m sync.Map var wg sync.WaitGroup wg.Add(2) // goroutine 1: 向 sync.Map 中存储键值对 go func() { defer wg.Done() m.Store("key1", "value1") m.Store("key2", "value2") }() // goroutine 2: 从 sync.Map 中加载键值对 go func() { defer wg.Done() if v, ok := m.Load("key1"); ok { fmt.Println("value for key1:", v) } if v, ok := m.Load("key2"); ok { fmt.Println("value for key2:", v) } }() wg.Wait() } ``` 在上面的例子中,我们首先创建了一个 sync.Map 对象 m。然后在两个 goroutine 中同时访问这个对象,一个 goroutine 向其中存储键值对,另一个 goroutine 则从其中加载键值对。由于 sync.Map 是并发安全的,所以这两个 goroutine 可以并发地访问和修改 sync.Map,而不会出现竞争条件。 需要注意的是,虽然 sync.Map 是并发安全的,但它并不是用来替代普通的 map 的。如果你只是需要在某个 goroutine 中访问和修改一个 map,那么你应该使用普通的 map,因为 sync.Map 的性能会比较差。只有在需要多个 goroutine 并发地访问和修改一个 map 时,才应该考虑使用 sync.Map。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值