Go之sync.Cond

sync.Cond

Condition variables are based on mutual exclusion locks. Condition variables are not used to protect critical sections and shared resources. It is used to coordinate those threads that want to access shared resources. When the state of the shared resource changes, it can be used to notify threads blocked by the mutex.

Condition variable initialization is inseparable from the mutex, and some of its methods are also based on the mutex.

There are three methods provided by condition variables:

Wait for notification ( wait ), under the mutex lock;

Single notification ( signal ), after unlocking the mutex;

Broadcast notification ( broadcast ), after the mutex is unlocked;

var mailbox uint8

var lock sync.RWMutex

sendCond := sync.NewCond(&lock)

recvCond := sync.NewCond(lock.RLocker())

The variable mailbox represents the mailbox and is of type uint8. If its value is 0, it means there is no information in the mailbox, and when its value is 1, it means there is information in the mailbox. lock is a variable of type sync.RWMutex, which is a read-write lock, and can also be regarded as the lock on the mailbox.

In addition, based on this lock, two variables representing condition variables were created, named sendCond and recvCond. They are all of type * sync.Cond, and are also initialized by the sync.NewCond function.

Unlike sync.Mutex type and sync.RWMutex type, sync.Cond type is not available out of the box. We can only use the sync.NewCond function to create its pointer value. This function requires a parameter value of type sync.Locker .

The condition variable is based on a mutex lock. It must be supported by a mutex lock to work. Therefore, the parameter value here is indispensable, it will participate in the implementation of the condition variable method .

sync.Locker is actually an interface , and only two method definitions are included in its declaration, namely: Lock () and Unlock ().

The sync.Mutex type and sync.RWMutex type both have Lock method and Unlock method, but they are all pointer methods. Therefore, the pointer types of these two types are the implementation types of the sync.Locker interface.

When the sendCond variable is initialized, the pointer value based on the lock variable is passed to the sync.NewCond function. The reason is that the Lock method and Unlock method of the lock variable are used to lock and unlock the write lock, respectively, and they correspond to the meaning of the sendCond variable. sendCond is a condition variable specially prepared for placing information. Putting information in a mailbox can be regarded as a write operation to a shared resource .

The recvCond variable represents the condition variable specially prepared for obtaining information . Although obtaining information will also involve changing the status of the mailbox, fortunately, only one person will do this, and we also need to take a look at the combination of condition variables and read locks in read-write locks. . So, here, for the time being, we think of obtaining intelligence as a read operation on shared resources.

Therefore, in order to initialize the condition variable recvCond, we need a read lock in the lock variable, and also need to be of type sync.Locker .

However, the methods used to lock and unlock the read lock in the lock variable are RLock and RUnlock, which do not match the methods defined in the sync.Locker interface. The RLocker method of sync.RWMutex type can fulfill this requirement. We only need to pass the result value of the call expression lock.RLocker () when calling the sync.NewCond function, so that the function can return the condition variable that meets the requirements.

Why is the value obtained by lock.RLocker () the read lock in the lock variable ? In fact, the Lock method and Unlock method possessed by this value will internally call the RLock method and RUnlock method of the lock variable. In other words, the first two methods are only agents of the latter two methods.

We now have four variables. One is the mailbox representing the mailbox, and the other is the lock representing the lock on the mailbox. There are also two, sendCond, which represents a child in a blue hat, and recvCond, which represents a child in a red hat.

 

What does the Wait method of condition variables do?

The Wait method of condition variables does four main things.

1. Add the calling goroutine (that is, the current goroutine) to the notification queue of the current condition variable.

2. Unlock the mutex on which the current condition variable is based.

3. Leave the current goroutine in a waiting state and decide whether to wake it up when the notification arrives. At this point, the goroutine will block on the line of code that calls the Wait method.

4. If the notification comes and decides to wake up the goroutine, then re-lock the mutex based on the current condition variable after waking it up. Since then, the current goroutine will continue to execute the following code.

Because the Wait method of the condition variable will unlock the mutex it is based on before blocking the current goroutine, before calling the Wait method, we must first lock the mutex, otherwise it will be raised when the Wait method is called An unrecoverable panic.

Why should the Wait method of condition variables do this? You can imagine that if the Wait method blocks the current goroutine while the mutex is already locked, who will unlock it? Any other goroutine?

Not to mention that this violates the important principle of mutual exclusion locks, namely: paired locking and unlocking, even if other goroutines can be unlocked, what if the unlocking is repeated? The panic caused by this cannot be recovered.

If the current goroutine cannot be unlocked, and none of the other goroutines will be unlocked, who will enter the critical section and change the state of the shared resource? As long as the state of the shared resource remains unchanged, even if the current goroutine is woken up due to the notification, it will still execute the Wait method again and be blocked again.

Therefore, if the Wait method of the condition variable does not first unlock the mutex, then it will only cause two consequences: either the current program crashes due to panic, or the related goroutine is completely blocked.

The if statement will only check the state of the shared resource once, but the for statement can do multiple checks until the state changes. Why do you have to do multiple inspections?

This is mainly for insurance purposes. If a goroutine is awakened by receiving a notification but finds that the state of the shared resource still does not meet its requirements, then it should call the Wait method of the condition variable again and continue to wait for the next notification.

There are multiple goroutines waiting for the same state of shared resources. For example, they all change the value of the mailbox variable to 0 when the value of the mailbox variable is not 0, which is equivalent to having multiple people waiting for me to place information in the mailbox. Although there are multiple waiting goroutines, only one goroutine can be successful at a time. The Wait method of the condition variable will relock the mutex after the current goroutine wakes up. After the successful goroutine finally unlocks the mutex, the other goroutine will enter the critical section one after another, but they will find that the state of the shared resource is still not what they want. At this time, the for loop is necessary.

There may not be two states for shared resources, but more. For example, the possible values ​​of the mailbox variable are not only 0 and 1, but also 2, 3, and 4. In this case, since there can only be one result after each change of state, under the premise of reasonable design, a single result must not satisfy all the conditions of goroutine. Those unsatisfied goroutine obviously still need to continue to wait and check.

There is a possibility that there are only two states of shared resources, and only one goroutine is concerned in each state, just like the example we implemented in the main problem. However, even so, it is still necessary to use the for statement. The reason is that, in some computer systems with multiple CPU cores, even if the condition variable is not notified, the goroutine calling its Wait method may be awakened. This is determined by the level of computer hardware, even the condition variables provided by the operating system (such as Linux) itself.

In summary, when using the Wait method of wrapping condition variables, we should always use the for statement .

 

What are the similarities and differences between the Signal method of the condition variable and the Broadcast method?

The Signal method and Broadcast method of condition variables are used to send notifications. The difference is that the former notification will only wake up a goroutine waiting for it, while the latter notification will wake up all the goroutines waiting for it.

The Wait method of the condition variable will always add the current goroutine to the end of the notification queue, and its Signal method will always start from the head of the notification queue to find the goroutine that can be woken up. Therefore, the goroutine that is awakened due to the notification of the Signal method is generally the one that waits the earliest.

The behavior of these two methods determines their applicable scenarios. If you are sure that there is only one goroutine waiting for notification, or just awaken any goroutine to meet the requirements, then use the Signal method of the condition variable.

Otherwise, it is always correct to use the Broadcast method, as long as you set the shared resource status expected by each goroutine.

In addition, once again, unlike the Wait method, the Signal method and Broadcast method of the condition variable do not need to be executed under the protection of the mutex. On the contrary, we better call these two methods after unlocking the mutex on which the condition variable is based. This is more conducive to the operating efficiency of the program.

Please note that the notification of condition variables is immediate. In other words, if no goroutine waits for this when sending a notification, the notification will be discarded directly. Goroutines that start waiting after this can only be woken up by subsequent notifications.

package sync

import (
	"sync/atomic"
	"unsafe"
)

// Cond implements a condition variable, a rendezvous point
// for goroutines waiting for or announcing the occurrence of an event.
// 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.

type Cond struct {
	noCopy noCopy

	// L is held while observing or changing the condition
	L Locker

	notify  notifyList
	checker copyChecker
}

// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
	return &Cond{L: l}

func (c *Cond) Wait() {
	c.checker.check()
	t := runtime_notifyListAdd(&c.notify)
	c.L.Unlock()
	runtime_notifyListWait(&c.notify, t)
	c.L.Lock()
}

// Signal wakes one goroutine waiting on c, if there is any.
// It is allowed but not required for the caller to hold c.L during the call.
func (c *Cond) Signal() {
	c.checker.check()
	runtime_notifyListNotifyOne(&c.notify)
}

// Broadcast wakes all goroutines waiting on c.
// It is allowed but not required for the caller to hold c.L during the call.
func (c *Cond) Broadcast() {
	c.checker.check()
	runtime_notifyListNotifyAll(&c.notify)
}

// copyChecker holds back pointer to itself to detect object copying.
type copyChecker uintptr

func (c *copyChecker) check() {
	if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
		!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
		uintptr(*c) != uintptr(unsafe.Pointer(c)) {
		panic("sync.Cond is copied")
	}
}
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}

 

Published 127 original articles · Likes 24 · Visits 130,000+

Guess you like

Origin blog.csdn.net/Linzhongyilisha/article/details/105471955