golang源码学习之sync.Cond

885 阅读4分钟

一、核心结构

// 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

image.png

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

执行完之后

image.png

三、总结

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