golang Sync包剖析

Go Sync包

sync.Map

  • sync.Map主要针对于Map对于并发读写不支持的场景下提出实现的,其原理是通过对map的写操作进行加锁:Sync.RWMutex
  • 同时sync.Map实现了读写分离,当对map进行读操作时,通过读read Map, 当read Map中不存在是去dirty map中读取

sync.Map的核心数据结构如下:

type Map struct {
	me Mutex
	read atomic.Value  // readOnly,读数据
	dirty map[interface{}]*entry // 包含最新的写入数据,当missed达到一定的值时,将值赋给read
	misses int  // 计数作用,每次从read中读失败,则missed加一
}

// readOnly的数据结构
type readOnly struct{
	m map[interface{}]*entry
	amended bool  // Map.dirty中的数据和这里的m中的数据不同时值为true
}

// entry的数据结构:
type entry struct {
	p unsafe.Pointer // *interface{}
	// 可见value是一个指针值,虽然read和dirty存在冗余情况,但由于是指针类型,存储空间不会太多
}

sync.Map相关问题

  1. sync.Map的核心实现:两个map,一个用于写,一个用于读,这样的设计思想可以类比于缓存与数据库
  2. sync.Map的局限性:如果写远高于读,dirty -> readOnly这个类似于刷新数据的频率较高,不如直接使用mutex + map的效率高
  3. sync.Map的设计思想:保证高频率读的无锁结构,空间换时间的思想

sync.Map包的简单使用:

func main(){
    var m sync.Map
    m.Store("a", 1)
    fmt.Println(m.Load("a"))
    // Tip: LoadOrStore存在就加载数据,没有就保存数据
    fmt.Println(m.LoadOrStore("a", 2))
    m.Delete("a")
    fmt.Println(m.LoadOrStore("a", 3))
    fmt.Println(m.LoadOrStore("b", 4))
    // 迭代map
    m.Range(func (key, value interface{}) bool {
        fmt.Println(key, value)
        return true
    })
}

sync.Cond

  • sync.Cond实现了在Locker的基础上进行消息广播和单播的功能。其内部维护一个等待队列,队列中存放的是所有等待sync.Cond的goroutine,及保存了
    一个通知队列。sync.Cond可以用来唤醒一个或所有因等待条件而阻塞的goroutine,以此来实现以此来实现多个goroutine之间的同步.

sync.Cond的结构体如下:

type Cond struct {
	noCopy npCopy // noCopy嵌入到结构体中,第一次使用之后不可复制,使用go vet作为检测使用
	L Locker  // 根据需求初始化不同的锁,如*Mutex, *RWMutex
	notify notifyList // 通知列表,调用wait()方法的goroutine会被放入到list中,每次唤醒从这里取出
	checker copyChecker //复制检查,检查cond实例是否被复制
}

wait函数实现:必须在当前协程获取锁之后才能Wait,Wait方法会在调用时先释放底层锁Locker,并且将当前的goroutine挂起,直到
另一个goroutine执行Signal或者Broadcast,该goroutine才有机会重新被唤醒,并尝试获取Locker

func (c *Cond) Wait(){
	// 检查cond是否是被复制的,如果是就panic掉
	c.checker.Check()
	// 将当前goroutine加入等待队列
	t := runtime_notifyListAdd(&c.notify)
	// 解锁
	c.L.UnLock()
	// 等待队列中所有的goroutine执行唤醒
	runtime_notifyListWait(&c.notify,t)
	c.L.Lock()
}

Signal函数:任意唤醒队列中的一个goroutine

func (c *Cond) Signal(){
    // 检查cond是否是被复制的,如果是就panic掉
	c.checker.Check()
	//通知等待队列中的一个goroutine被唤醒
	runtime_notifyListNotifyOne(&c.notify)
}

Broadcast函数:唤起等待队列中所有的goroutine

func (c *Cond) Broadcast(){
    // 检查cond是否是被复制的,如果是就panic掉
    c.checker.Check()
    runtime_notifyListNotifyAll(&c.notify)
}

sync.Cond的相关问题

  1. sync.Cond的核心实现:通过一个锁,封装了notify通知的实现,包括了单个通知和广播两种方式
  2. sync.Cond与Channel的异同:channel应用于一收一发的场景,sync.Cond应用于多收一发的场景

sync.Cond的简单使用示例:

func main(){
	var m sync.Mutex
	c := sync.NewCond(&m)

	// Tip: 主协程先获得锁
	c.L.Lock()
	go func(){
		// Tip: 协程一开始无法获得锁
		c.L.Lock()
		defer c.L.Unlock()
		fmt.Println("协程获得锁")
		time.Sleep(time.Second)
		// Tip: 通过notify进行广播通知
		c.Broadcast()
		fmt.Println("协程执行完毕,即将执行defer中的解锁操作")
	}()
	fmt.Println("主协程获得锁")
	time.Sleep(time.Second)
	fmt.Println("主协程即将释放掉锁,此时仍然占有")
	// Tip: Wait的实现,先释放锁,直到收到了notify,又进行加锁
	c.Wait()
	c.L.Unlock()
	fmt.Println("Done")
}

Sync.Pool

  • 由于Golang GC对频繁创建取消对象的性能影响,及对于使用频繁但使用周期不长的对象,为了减少GC,golang提供了对象重用机制,
    也就是sync.Pool对象池。sync.Pool是可以伸缩的,并发安全的。其大小仅仅受限于内存大小,可以被看作是一个存放重用对象的容器。目的是
    为了存放已经分配但是暂时不用的对象,在需要的时候直接从pool中取出

sync.Pool的底层数据结构:

type Pool struct {
	noCopy noCopy
	local unsafe.Pointer  // 固定大小的per-P池,实际类型为[P]poolLocal
	localSize uintprt // local array的大小
	New func()interface{} // New方法在Get失败的情况下,选择性的创建一个只,否则返回nil
}

type PoolLocal struct {
	poolLocalInternal
	// 将poolLocal补齐至两个缓存行的倍数,防止false sharing(伪共享)
	// 每个缓存行具有64bytes, 及512bit
	pad [128 - unsafe.Sizeof(poolLocalInternal{}) % 128]byte
}

type poolLocalInternal struct {
	private interface{} // 只能被局部调度器P使用
	shared []interface{} // 所有P共享
	Mutex // 访问共享数据域的锁
}
  • 为了使在多个goroutine中高效的使用对象,sync.Pool为每个P(process)都分配一个localPool,当执行Get或者Put的时候,会先将goroutine和某个
    P的localPool相关联,再对该子池进行操作。每个P的子池分为私有对象和共享列表对象,私有对象只能被特定的P访问,共享列表对象可以被任何P访问。因为同一时刻一个P只能
    执行一个goroutine,所以无需加锁,但对共享列表对象进行操作时,则需要加锁。

Put函数实现:Put的过程就是将零时对象放进pool:

  1. 如果放入的值为nil直接返回
  2. 检查当前goroutine是否设置对象私有池的值,如果没有则将x赋值给其私有成员,并将x设置为nil
  3. 如果当前goroutine私有值已经被设置,那么将该值追加到共享列表
func (p *Pool)Put(x interface{}){
	if x == nil {
		return
    }
    // 获取localPool
    l := p.pin()
    // 优先放入private
    if l.private == nil {
    	l.private = x
    	return
    }
    runtime_procUnpin()
    // 如果不能放入private,则放入shared
    if x != nil {
    	l.Lock()
    	l.shared = append(l.shared, x)
    	l.Unlock()
    }
}

Get函数实现:在从池中获取对象的时候,会先从per-P的poolLocal slice中选取一个poolLocal

  1. 尝试从本地P对应的那个本地池中获取一个对象值,并从本地池中删除该值
  2. 如果获取失败,那么从共享池中获取,并从共享池中删除该值
  3. 如果获取失败,那么从其他P的共享池中获取,并删除共享池中的该值
  4. 如果仍然失败,那么直接通过New()分配一个返回值,注意这个分配的值不会被放入到池中。New()返回用户注册的New()函数的值,如果用户未注册,那么返回nil
func (p *Pool) Get() interface{} {
	// 现获取poolLocal
	l := p.pin()
	// 先从private中去取
	x := l.private
	l.private = nil
	runtime_procUnpin()
	// private不存在再从shared中去取
	if x == nil {
		// 加锁,从shared中获取
		l.Lock()
		// 从shared尾部取缓存对象
		last := len(l.shared) - 1
		if last >= 0 {
			x = l.shared[last]
			l.shared = l.shared[:last]
        }
        l.Unlock()
		if == nil {
			// 如果取不到,则获取新的缓存对象,从其他的P共享池中获取
			x = p.getSlow()
        }
    }
    // 如果getSlow还是获取不到,则New一个
    if x == nil && p.New = nil {
    	x = p.New()
    }
	return x
}
  • 在go 1.3版本之后,取消了mutex的实现,该用了poolChain实现SPMC无锁队列,避免了锁的开销,mutex变成了atomic. 同时新增了victim cache,其目的是
    在GC处理过程中直接回收oldPools的对象,GC处理是并不直接将AllPools的对象直接进行GC,而是保存到oldPools,等到下一次GC周期到了再处理
    这样的优势是增加了Get获取内存的选项,增加了对象复用的概率,时间上通过延迟GC,增加对象复用的时间长度。

Sync.Pool相关问题:

  1. sync.Pool的核心作用:缓存稍后会频繁使用的对象 + 减轻GC压力
  2. sync.Pool的Put与Get: Put的顺序为local private -> local shared, Get的顺序为local private -> local shared -> remote shared
  3. 思考sync.Pool应用的核心场景:高频使用且生命周期短的对象,且初始化始终一致,如fmt
  4. 1.3中引入victim的作用: victim cache机制

sync.Pool的简单使用

func main(){
	var sp = sync.Pool{
		// Tip: 声明对象池的New函数,这里以一个简单的int为例
		New: func() interface{} {
			return 100
		},
	}
	// Tip: 从对象池中获取一个对象
	data := sp.Get().(int)
	fmt.Println(data)
	// Tip: 往对象池中放回一个对象
	sp.Put(data)
}

Sync.Once

  • sync.Once被用于控制变量的初始化,这个变量的读写通常遵循单例模式:
  1. 当且仅当第一次读某个变量时进行初始化
  2. 变量被初始化的过程中,所有的读都被阻塞,当变量初始化写完之后,读操作继续
  3. 变量仅初始化一次,初始化后驻留在内存中
  • sync.Once作用与init函数类似,不同之处在于:
  1. init函数在文件包首次被家在的时候执行,且只执行一次
  2. sync.Once是在代码运行中需要的时候执行,且只执行一次

sync.Once的底层数据结构:sync.Once使用变量done来记录函数的执行状态,使用sync.Mutex和sync.atomic来保证线程安全的读取done

type Once {
	m Mutex
	done uint32  // 状态位:判断变量是否初始化完成,有效值为0, 1
}

Do函数实现:

func (o *Once) Do(f func()){
	if atomic.LoadUint32(&o.done) == 1 {
		return
    }
    o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUnit32(&o.done, 1)
		f()
    }
}

sync.Once的简单示例

func main(){
	var once sync.Once
	onceBody := func(){
		fmt.Println("Only Once")
	}
	done := make(chan bool)
	for i := 0; i < 10; i++{
		go func(){
			// Tip: 调用Do方法只执行一次onceBody方法
			once.Do(onceBody)
			done <- true
		}()
	}
	for i := 0; i < 10; i++{
		<-done
	}
}

sync.WaitGroup

  • sync.WaitGroup常用于针对goroutine的并发执行,通过WaitGroup可以等待所有的go程序执行结束之后再执行之后的逻辑
  • WaitGroup对象内部有一个计数器,最初重0开始,提供了三个方法:Add(),Done(),Wait()用来控制计数器的数量。Add(n)把计数器设置为n,
    Done()每次把计数器减一,Wait()会阻塞代码的执行,直到计数器的值减到0为止。

Add函数实现:

func (wg *WaitGroup) Add(delta int){
	statep := wg.state()
	// 更新statep,state将在wait和add中通过原子操作一起使用
	state := atomic.AddUint64(statep, uint64(delta)<<32)
	v := int32(state >> 32)
	w := uint32(state)
	if v < 0 {
		panic("sync: negative WaitGroup Counter")
    }
    if w != 0 && delta > 0 && v == int32(delta) {
    	// wait不等于0说明已经执行了wait,此时不允许Add()
    	panic("sync: WaitGroup misuse: Add Called concurrently with Wait")
    }
    // 正常情况下,Add会让v增加,Done会让v减少,如果没有全部Done掉,此时v总是会大于0的,知道v等于0才往下走
    // 而w代表有多少个goroutine在等待信号,wait中通过compareAndSwap对这个w进行加一
    if v > 0 || w == 0{
    	return
    }
    // 当v为0或者w不为0才会到这里,但是这个过程中又有一次Add,导致panic
    if *statep != state {
    	panic("sync: WaitGroup misuse: Add Called concurrently with Wait")
    }
    *statep = 0
    // 将信号发出,出发wait()结束
    for ; w != 0; w-- {
    	runtime_Semrelease(&wg.sema, false)
    }
}

Done函数实现:

func (wg *WaitGroup) Done(){
	wg.Add(-1)
}

Wait函数实现:

func (wg *WaitGroup) Wait(){
	statep := wg.state()
	for {
		state := atomic.LoadUint64(statep)
		v := int32(state >> 32)
		w := uint32(state)
		if v == 0 {
			if race.Enabled {
				race.Enable()
				race.Acquire(unsafe.Pointer(wg))
            }
            return
        }
        // 如果statep和state相等,则增加等待计数,同时进入if等待信号量
        // 此处做CAS, 主要是防止多个goroutine里进行Wait()操作,每又一个goroutine进行Wait,等待计数就加一
        // 这里如果不想等,说明statep在从读出来到CAS比较的时间内,被别的goroutine改写了,那么就不进入if,回去在读一次,这样避免使用锁,更高效
        if atomic.CompareAndSwapUint64(statep, state, state + 1){
        	if race.Enabled && w == 0 {
        		race.Write(unsafe.Pointer(&wg.sema))
            }
            // 等待信号量
            runtime_Semacquire(&wg.sema)
        	// 信号量到来,代表所有的Add都Done了
        	if *statep != 0 {
        		panic("sync: WaitGroup is reused before previous Wait has returned")
            }
            return
        }
    }
}

sync.WaitGroup的简单使用

func main(){
  wg := sync.WaitGroup{}
  // Tip: 计数器加100
  wg.Add(100)
  for i := 0; i < 100; i++{
    go func(i int){
      fmt.Println("这是第" + strconv.Itoa(i) + "个")
      // Tip: 每次Done -1
      wg.Done()
    }(i)
  }
  wg.Wait()
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值