Dawn's Blogs

分享技术 记录成长

0%

Go语言高性能编程 (5) 并发编程——Pool Once Cond

sync.Pool

sync.Pool 用于复用对象,sync.Pool 是可伸缩的,同时也是并发安全的,其大小仅受限于内存的大小。从 sync.Pool 中取出的值不一定每一次都会经过内存分配,可以复用之前已经创建过已有但是已经使用结束的对象

简单来说,就是保存和复用临时对象,减少内存分配,降低 GC 压力

sync.Pool 使用只需要知道三点即可:

  • 声明对象池时,需要指定 New 函数,对象池中没有对象时,将会调用 New 函数创建。
  • Get 用于从对象池中获取一个对象,Put 用于向对象池内放回一个对象。

如果在需要频繁创建对象的场景下,sync.Pool 有两个优点

  • 节省了对象初始化的时间,因为每一次都不一定是新的对象所以可以在一定程度上节省对象初始化时间,提升了效率。
  • 节省内存分配,对象是可重用的,不必每一次都分配内存。

sync.Once

sync.Once 保证了只执行一次函数,常常用于单例模式,在前文中也可以用于关闭管道。sync.Once 仅提供了一个方法 Do,参数 f 是对象初始化函数:

1
func (o *Once) Do(f func())

原理实现

从 sync.Once 的功能可以想到,用于实现它需要两个部分:

  • 一个标志判断是否已经执行了 Do,如果没有执行过 Do 则执行对象初始化函数。
  • 线程安全,并发的判断是否已经执行了,如果没有则执行 Do 中定义的函数,所以需要互斥锁来实现。

以下是 sync.Once 的源码实现,其中 Once 结构体包含两个字段:

  • done 字段用于标记是否已经执行过。
  • m 字段为互斥锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Once struct {
done uint32
m Mutex
}

func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}

func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
  1. 热路径 (hot path) 是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 o.done,在热路径上是比较好理解的,如果 hot path 编译后的机器码指令更少,更直接,必然是能够提升性能的。
  2. 为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移 (calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因此,访问第一个字段的机器代码更紧凑,速度更快。

sync.Cond

sync.Cond 与互斥锁不用,它用于协程之间的同步。协调想要访问共享资源的那些 goroutine,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻塞的 goroutine。

sync.Cond 基于互斥锁/读写锁,那么它和互斥锁之间有什么区别?

  • 互斥锁通常用于保护临界区和共享资源,条件变量 sync.Cond 用来协调想要访问共享资源的 goroutine
  • sync.Cond 经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。

四个方法

sync.Cond 有四个相关方法:

  • NewCond 创建实例:NewCond 创建 Cond 实例时,需要关联一个锁。
  • Broadcast 广播唤醒所有:Broadcast 唤醒所有等待条件变量 c 的 goroutine,无需锁保护。
  • Signal 唤醒一个:Signal 只唤醒任意 1 个等待条件变量 c 的 goroutine,无需锁保护。
  • Wait 等待:调用 Cond.Wait 方法时必须加锁。

Cond.Wait 的使用需要参照以下模板:

1
2
3
4
5
6
c.L.Lock()
for !condition() {
c.Wait()
}
// ... make use of condition ...
c.L.Unlock()

实现

数据结构

sync.Cond 的定义如下:每一个 Cond 都会关联一个锁,当条件改变或者调用 Cond.Wait 方法时必须加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Each Cond has an associated Locker L (often a *Mutex or *RWMutex),
// which must be held when changing the condition and
// when calling the Wait method.
//
// A Cond must not be copied after first use.
type Cond struct {
noCopy noCopy // Cond使用后不允许拷贝
// L is held while observing or changing the condition
L Locker

//通知列表调用wait()方法的goroutine会被放到notifyList中
notify notifyList
checker copyChecker //检查Cond实例是否被复制
}

而 notifyList 的定义如下,包含三类字段:

  • wait 和 notify 两个无符号整型,分别表示了 Wait() 操作的次数和 goroutine 被唤醒的次数。
  • lock 是系统内部运行时实现的一个简单版本的互斥锁。
  • tail 和 head 维护了阻塞在当前 sync.Cond 上的 goroutine 链表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type notifyList struct {
// wait is the ticket number of the next waiter. It is atomically
// incremented outside the lock.
wait uint32 // 等待goroutine操作的数量

// notify is the ticket number of the next waiter to be notified. It can
// be read outside the lock, but is only written to with lock held.
//
// Both wait & notify can wrap around, and such cases will be correctly
// handled as long as their "unwrapped" difference is bounded by 2^31.
// For this not to be the case, we'd need to have 2^31+ goroutines
// blocked on the same condvar, which is currently not possible.
notify uint32 // 唤醒goroutine操作的数量

// List of parked waiters.
lock mutex
head *sudog
tail *sudog
}

img

Wait 实现

至于调用 Cond.Wait 方法时必须加锁,就需要查看 Wait 的实现原理了。

Cond.Wait 方法在挂起 goroutine 期间,会在 Wait 方法内部释放锁,所以需要在调用 Cond.Wait 方法时加锁。

1
2
3
4
5
6
7
8
9
10
11
12
func (c *Cond) Wait() {
//1. 检查cond是否被拷贝
c.checker.check()
//2. notifyList.wait+1
t := runtime_notifyListAdd(&c.notify)
//3. 释放锁 让出资源给其他goroutine
c.L.Unlock()
//4. 挂起goroutine
runtime_notifyListWait(&c.notify, t)
//5. 尝试获得锁
c.L.Lock()
}