28 Conditional Variables sync #
Hello, this is Haolin. Today, I will continue to share more about the usage of conditional variables sync.Cond. We will pick up where we left off in the previous article and expand our knowledge.
Question 1: What does the Wait
method of a condition variable do?
#
After understanding how to use a condition variable, you might have a few questions:
- Why do we need to lock the mutex associated with the condition variable before calling its
Wait
method? - Why do we need to use a
for
loop to wrap the expression that calls theWait
method? Can’t we use anif
statement?
I often ask these questions during interviews. To answer them, you need to understand how the Wait
method works internally.
The Wait
method of a condition variable mainly does four things:
- Adds the calling goroutine (i.e., the current goroutine) to the notification queue of the condition variable.
- Unlocks the mutex associated with the condition variable.
- Puts the current goroutine in a waiting state until a notification arrives and decides whether to wake it up. At this point, the goroutine will block on the line of code that calls this
Wait
method. - If a notification arrives and it is decided to wake up the goroutine, the mutex associated with the current condition variable is locked again. After this, the current goroutine will continue executing the subsequent code.
Do you now know the answer to the first question I mentioned?
Because the Wait
method of a condition variable unlocks the mutex it is based on before blocking the current goroutine, we must lock that mutex before calling the Wait
method. Otherwise, calling this Wait
method would result in an unrecoverable panic.
Why does the Wait
method of a condition variable work this way? You can imagine that if the Wait
method blocks the current goroutine while the mutex is already locked, who will unlock it? Another goroutine?
Setting aside the fact that this violates the important principle of using mutexes, i.e., locking and unlocking in pairs, even if another goroutine were to unlock it, what if the unlocking is repeated? This would lead to an unrecoverable panic.
If the current goroutine is unable to unlock, and no other goroutine unlocks it either, who will enter the critical section and change the state of shared resources? As long as the state of the shared resources remains unchanged, even if the current goroutine is awakened due to a notification, it will still execute the Wait
method again and be blocked again.
Therefore, if the Wait
method of a condition variable does not unlock the mutex first, it will only result in two consequences: either the program crashes due to a panic or all relevant goroutines are completely blocked.
Now let’s explain the second question. It is clear that an if
statement performs a condition check on the state of shared resources only once, while a for
loop can perform multiple checks until the state changes. Why do we need multiple checks?
This is mainly for safety. If a goroutine is awakened due to a notification but finds that the state of the shared resources still does not meet its requirements, it should call the Wait
method of the condition variable again and continue waiting for the next notification.
This situation is very likely to occur, as shown below:
-
Multiple goroutines are waiting for the same state of shared resources. For example, they are all waiting for the value of the
mailbox
variable to become non-zero before changing it to0
. This is like multiple people waiting for me to put information in the mailbox. Although there are multiple goroutines waiting, only one goroutine can be successful each time. Remember that theWait
method of a condition variable first relocks the mutex after the awakened goroutine, and after the successful goroutine eventually unlocks the mutex, the other goroutines will enter the critical section one by one. However, they will find that the state of the shared resources still does not meet their requirements. In this case, afor
loop is necessary. -
The state of shared resources may have more than two possible states. For example, the
mailbox
variable may have values other than0
and1
, such as2
,3
, and4
. In this case, due to the fact that the state can only have one result after each change, a single result alone cannot satisfy all goroutines, provided the design is reasonable. Those unsatisfied goroutines obviously need to continue waiting and checking. -
It is possible that the state of shared resources has only two possible states and only one goroutine is interested in each state, just like the example we implemented in the main question. However, even in this case, using a
for
loop is still necessary. The reason is that on some multi-CPU core computer systems, even if no notification is received for the condition variable, the goroutine that calls itsWait
method can still be awakened. This is determined by the computer hardware and even the condition variables provided by the operating system (such as Linux) behave in the same way.
In summary, when wrapping the Wait
method of a condition variable, we should always use a for
loop.
At this point, I think you should have enough knowledge about the Wait
method of a condition variable.
Question 2: What are the similarities and differences between the Signal
method and the Broadcast
method of a conditional variable?
#
The Signal
method and the Broadcast
method of a conditional variable are both used to send notifications. The difference is that the former is used to wake up only one goroutine that is waiting, while the latter is used to wake up all goroutines that are waiting.
The Wait
method of the conditional variable always adds the current goroutine to the end of the notification queue, while its Signal
method always starts searching for a waiting goroutine to wake up from the beginning of the notification queue. Therefore, the goroutine that is woken up by the notification from the Signal
method is usually the one that has been waiting the longest.
The behavior of these two methods determines their applicable scenarios. If you are sure that only one goroutine is waiting for the notification, or waking up any goroutine will meet the requirement, then you can use the Signal
method of the conditional variable.
Otherwise, using the Broadcast
method is always correct, as long as you set the expected shared resource status for each goroutine.
In addition, it is worth emphasizing again that, unlike the Wait
method, the Signal
method and the Broadcast
method of the conditional variable do not need to be executed under the protection of a mutex lock. On the contrary, it is better to call these two methods after unlocking the mutex on which the conditional variable is based. This is more conducive to the efficiency of program execution.
Finally, please note that the notifications of the conditional variable are immediate. In other words, if there is no goroutine waiting for the notification when it is sent, the notification will be discarded directly. Goroutines that start waiting after that can only be woken up by subsequent notifications.
You can open the demo62.go file and carefully observe the differences between it and demo61.go. Pay special attention to the type of the lock
variable and the way the notifications are sent.
Summary #
Today, we mainly talked about condition variables, which are a synchronization tool based on mutex locks. In Go language, we need to use the sync.NewCond
function to initialize a condition variable of type sync.Cond
.
The sync.NewCond
function requires a parameter value of type sync.Locker
.
Values of type *sync.Mutex
as well as *sync.RWMutex
can both satisfy this requirement. Additionally, the latter’s RLocker
method can return the read lock in this value, which can also be used as the parameter value for the sync.NewCond
function, thus generating a condition variable corresponding to the read lock in the read-write lock.
The Wait
method of condition variables needs to be executed under the protection of the underlying mutex lock, otherwise it will cause an unrecoverable panic. In addition, it is best to use a for
statement to check the status of shared resources and wrap the call to the Wait
method of the condition variable.
Do not use if
statements because they cannot repeat the process of “check status – wait for notification – wake up”. The reason for repeating this process is that a goroutine that is “blocked due to waiting for notification” may be woken up when the state of the shared resource does not meet its requirements.
The Signal
method of the condition variable only wakes up one goroutine that is blocked due to waiting for notification, while its Broadcast
method can wake up all goroutines waiting for this. The latter is much more versatile than the former.
These two methods do not need to be protected by the mutex lock, and it is also best not to call them before unlocking the mutex lock. Additionally, the notification of condition variables is immediate. When a notification is sent, if no goroutine needs to be awakened, the notification will be immediately invalidated.
Thought question #
What is the purpose of the public field L
in the sync.Cond
type? Can we change the value of this field during the use of a condition variable?
Click here to view detailed code for the Go language column.