一、核心结构
// Cond implements a condition variable, a rendezvous point
// for goroutines waiting for or announcing the occurrence
// of an event.
// 等待或宣布事件发生的goroutine的集合点
// 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
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}
里面主要有几个成员:
1、onCopy是用来配合go vet做静态编译检查,标识Cond在第一次使用之后不能被copy
2、locker是在Wait函数被并发调用时候,保证数据正确性的,应为底层的实现并不是原子的
3、notify是核心的数据结构,一个链表
// Approximation of notifyList in runtime/sema.go. Size and alignment must
// agree.
type notifyList struct {
wait uint32
notify uint32
lock uintptr // key field of the mutex
head unsafe.Pointer
tail unsafe.Pointer
}
notifyList和runtime/sema.go的结构是对应的
// notifyList is a ticket-based notification list used to implement sync.Cond.
//
// It must be kept in sync with the sync package.
type notifyList struct {
// wait is the ticket number of the next waiter. It is atomically
// incremented outside the lock.
// wait代表next watier的ticket.他在lock之外原子自增
wait uint32
// 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是下一个要通知的goroutine的票号。它可以在锁之外读取,但是只能在持有锁的情况下写入。
notify uint32
// List of parked waiters.
// 互斥锁
lock mutex
// g链表的头
head *sudog
// g链表的尾
tail *sudog
}
4、checker的目的也是用来防止copy
二、核心函数
1、Wait
func (c *Cond) Wait() {
// check一下是否被copy,被copy会panic
c.checker.check()
// notifyListAdd adds the caller to a notify list such that it can receive
// notifications. The caller must eventually call notifyListWait to wait for
// such a notification, passing the returned ticket number.
// 追加goroutine到通知列表里面,以便这个goroutine可以收到通知。
// 调用者必须最终调用notifyListWait来等待这样的通知,并传递返回的票证号
// This may be called concurrently, for example, when called from
// sync.Cond.Wait while holding a RWMutex in read mode.
// 由于底层atomic.Xadd(&l.wait, 1) - 1是两个操作,所以需要加锁保证原子性
// 返回的t是通知票据
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
// notifyListWait waits for a notification. If one has been sent since
// notifyListAdd was called, it returns immediately. Otherwise, it blocks.
// 等待通知,如果从notifyListAdd被调用之后,有通知已经发出,那么notifyListWait立即返回,否则就会block
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
这个函数主要是利用runtime/sema.go的函数实现
runtime_notifyListAdd-->notifyListAdd
func notifyListAdd(l *notifyList) uint32 {
// This may be called concurrently, for example, when called from
// sync.Cond.Wait while holding a RWMutex in read mode.
return atomic.Xadd(&l.wait, 1) - 1
}
可以看到这个函数很简单,就是notifyList里面的wait加1,返回未操作之前的值,因为这里的操作不是原子性的,所以在并发场景下,会出现问题,因此在Cond中,通过Locker来保障并发数据的安全
runtime_notifyListWait-->notifyListWait
func notifyListWait(l *notifyList, t uint32) {
lockWithRank(&l.lock, lockRankNotifyList)
// Return right away if this ticket has already been notified.
if less(t, l.notify) {
unlock(&l.lock)
return
}
// Enqueue itself.
s := acquireSudog()
s.g = getg()
s.ticket = t
s.releasetime = 0
t0 := int64(0)
if blockprofilerate > 0 {
t0 = cputicks()
s.releasetime = -1
}
if l.tail == nil {
l.head = s
} else {
l.tail.next = s
}
l.tail = s
goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3)
if t0 != 0 {
blockevent(s.releasetime-t0, 2)
}
releaseSudog(s)
}
这个函数的目的主要是用来阻塞当前goroutine,首先是判断wait和notify的大小,前面提到过,wait代表下一个等待唤醒的goroutine的ticket,nofity表示是下一个去唤醒的goroutine的ticket,如果wait<nofity,已经被唤醒过了,不需要阻塞。后面执行的操作就是,获取当前的g,把分配的ticker交给这个g,然后把g追加到链表的tail
// Puts the current goroutine into a waiting state and unlocks the lock.
// The goroutine can be made runnable again by calling goready(gp).
func goparkunlock(lock *mutex, reason waitReason, traceEv byte, traceskip int) {
gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceskip)
}
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
goparkunlock这个函数作用是把g调整为waiting状态并且释放lock,这个g可以通过goready函数重新进入runnable状态,runnable会在notify的时候执行。
在Wait这个函数中add和wait是一起使用的,假设第一次g进入的时候,wait和notify都是0,在执行完add操作之后,将wait变成1,返回wait的初始值0,然后wait函数把0这个ticket传递给g
2、Signal
func (c *Cond) Signal() {
c.checker.check()
// notifyListNotifyOne notifies one entry in the list.
runtime_notifyListNotifyOne(&c.notify)
}
runtime_notifyListNotifyOne-->notifyListNotifyOne
func notifyListNotifyOne(l *notifyList) {
// Fast-path: if there are no new waiters since the last notification
// we don't need to acquire the lock at all.
if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
return
}
lockWithRank(&l.lock, lockRankNotifyList)
// Re-check under the lock if we need to do anything.
t := l.notify
if t == atomic.Load(&l.wait) {
unlock(&l.lock)
return
}
atomic.Store(&l.notify, t+1)
for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
if s.ticket == t {
n := s.next
if p != nil {
p.next = n
} else {
l.head = n
}
if n == nil {
l.tail = p
}
unlock(&l.lock)
s.next = nil
readyWithTime(s, 4)
return
}
}
unlock(&l.lock)
}
这个函数目的是唤醒前面wait的g,首先是判断wait和notify,如果相等,说明没有新的在wait的g,不需要执行后续逻辑,直接放回。然后就是获取lock,进行后面的操作,在获取到锁执行,double-check一下wait和notify,防止并发场景下有其他g已经通知了。把notify+1,然后遍历等待链表,找到需要通知的ticket对应的g,执行readyWithTime
func readyWithTime(s *sudog, traceskip int) {
if s.releasetime != 0 {
s.releasetime = cputicks()
}
// 和前面wait里面的gopark对应
goready(s.g, traceskip)
}
执行完之后
三、总结
1、维护的wait和notify状态改变依赖Wait和Signal的执行,不考虑并发的情况下,执行Wait之后wait=nofity+1,然后执行Signal将nofity+1似使得waig=notify,这表明Wait必须要在Signal之前发生,不然就会出现deadlock
2、Cond一定是配置Lock一起使用的,在初始化的时候需要传递Lock
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
3、Wait和Lock一起使用
c.L.Lock()
for !condition() {
c.Wait()
}
... make use of condition ...
c.L.Unlock()