Detailed explanation of sync.cond in go language

Time:2021-9-25

What can sync.cond be used for?

Cond in golang’s sync package implements a condition variable that can use multiple readers to wait for public resources.

Each cond is associated with a lock. When modifying a condition or calling the wait method, it must be locked to protect the condition. It is similar to wait and notifyAll in Java.

The sync.cond condition variable is used to coordinate those goroutines that want to share resources. When the state of shared resources changes, it can be used to notify goroutines blocked by mutex.

Difference from sync.mutex

Sync.cond is based on mutex. What’s the difference between sync.cond and mutex?

Sync.mutex is usually used to protect critical areas and shared resources, and the condition variable sync.cond is used to coordinate the shared resources you want to access.

Sync.cond usage scenario

One process is receiving data, and other processes must wait for this process to receive data before they can read the correct data.

In the above case, if you simply use channel or mutex, only one process can wait and read the data. There is no way to notify other processes to read the data.

What about this time?

  • A global variable can be used to identify whether the first process has received data. The remaining processes repeatedly check the value of the variable until the data is read.
  • You can also create multiple channels. Each process is blocked on one channel. The process receiving data will notify one by one after the data is received.

Then go actually has a sync. Cond built in to solve this problem.

sync.Cond

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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
}

You can see that each cond is associated with a lock L (mutex, or read-write lock * rmmutex). The lock must be added when modifying conditions or using wait.

What are the methods of sync.cond

Newcond create instance

?
1
func NewCond(l Locker) *Cond

Newcond needs to associate a lock to create an instance.

Specific examples:

?
1
cadence := sync.NewCond(&sync.Mutex{})

Broadcast wakes up all

?
1
2
3
4
5
// 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()

Broadcast wakes up goroutine of all waiting condition variables C without lock protection.

Specific examples:

?
1
2
3
4
5
go func() {
   for range time.Tick(1 * time.Millisecond) {
      cadence.Broadcast()
   }
}()

Signal wakes up a process

?
1
2
3
4
5
// 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()

Signal only wakes up goroutine of any waiting condition variable C without lock protection. It is a bit similar to notify in Java

Wait wait

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Wait atomically unlocks c.L and suspends execution
// of the calling goroutine. After later resuming execution,
// Wait locks c.L before returning. Unlike in other systems,
// Wait cannot return unless awoken by Broadcast or Signal.
//
// Because c.L is not locked when Wait first resumes, the caller
// typically cannot assume that the condition is true when
// Wait returns. Instead, the caller should Wait in a loop:
//
//    c.L.Lock()
//    for !condition() {
//        c.Wait()
//    }
//    ... make use of condition ...
//    c.L.Unlock()
//
func (c *Cond) Wait()

Calling wait will automatically release the lock C.L and suspend the goroutine where the caller is located. Therefore, the current workflow will block where the wait method is called. If other processes call signal or broadcast to wake up the process, the wait method ends the blocking, locks C.L again, and continues to execute the code behind the wait

Code example:

?
1
2
3
4
5
6
c.L.Lock()
for !condition() {
    c.Wait()
}
... make use of condition ...
c.L.Unlock()

Code example

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package sync
 
import (
   "log"
   "sync"
   "testing"
   "time"
)
 
var done = false
 
func read(name string, c *sync.Cond) {
   c.L.Lock()
   for !done {
      c.Wait()
   }
   log.Println(name, "starts reading")
   c.L.Unlock()
}
 
func write(name string, c *sync.Cond) {
   log.Println(name, "starts writing")
   time.Sleep(time.Second)
   c.L.Lock()
   done = true
   c.L.Unlock()
   log.Println(name, "wakes all")
   c.Broadcast()
}
 
func TestSyncCond(t *testing.T) {
   cond := sync.NewCond(&sync.Mutex{})
 
   go read("reader1", cond)
   go read("reader2", cond)
   go read("reader3", cond)
   write("writer", cond)
 
   time.Sleep(time.Second * 3)
}

Operation results

=== RUN   TestSyncCond
2021/08/26 11:06:48 writer starts writing
2021/08/26 11:06:49 writer wakes all
2021/08/26 11:06:49 reader3 starts reading
2021/08/26 11:06:49 reader2 starts reading
2021/08/26 11:06:49 reader1 starts reading
— PASS: TestSyncCond (4.01s)
PASS

This is the end of this article about the detailed use of sync.cond in go language. For more information about go sync.cond, please search the previous articles of developeppaper or continue to browse the relevant articles below. I hope you will support developeppaper in the future!