28 Conditional Variables Sync. Cond Part 2

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:

  1. Why do we need to lock the mutex associated with the condition variable before calling its Wait method?
  2. Why do we need to use a for loop to wrap the expression that calls the Wait method? Can’t we use an if 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:

  1. Adds the calling goroutine (i.e., the current goroutine) to the notification queue of the condition variable.
  2. Unlocks the mutex associated with the condition variable.
  3. 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.
  4. 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:

  1. 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 to 0. 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 the Wait 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, a for loop is necessary.

  2. The state of shared resources may have more than two possible states. For example, the mailbox variable may have values other than 0 and 1, such as 2, 3, and 4. 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.

  3. 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 its Wait 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.