2

Go language through the go keyword to open goroutine so that developers can easily implement concurrent programming, and the effective operation of concurrent programs, often can not do without the escort of the sync package. Currently, the enablement list of the sync package includes: atomic operations under sync.atomic sync.Map concurrent security map, mutexes and read-write locks provided by sync.Mutex and sync.RWMutex sync.Pool multiplexed object pool, sync.Once singleton mode, sync.Waitgroup Mode, monitor mode of sync.Cond Of course, in addition to the sync package, there are channel and context with higher encapsulation levels.

To write a qualified Go program, the above concurrency primitives must be mastered. For most sync.Cond , 061795b72c8654 should be the most unfamiliar, this article will take a closer look.

First acquaintance with sync.Cond

sync.Cond literally means the synchronization condition variable, which implements a monitor mode.

In concurrent programming(also known as parallel programming), a monitor is a synchronization construct that allows threads to have both mutual exclusion and the ability to wait (block) for a certain condition to become false.

For Cond, it implements a condition variable, which is the point of waiting and notification between goroutines. The condition variable is isolated from the shared data. It can block multiple goroutines at the same time until another goroutine changes the condition variable and notify one or more blocked goroutines to wake up.

Readers who are in contact for the first time may not quite understand, so let's take a look at the demo code example in "Rethinking Classical Concurrency Patterns"

type Item = int

type Queue struct {
   items     []Item
   itemAdded sync.Cond
}

func NewQueue() *Queue {
   q := new(Queue)
   q.itemAdded.L = &sync.Mutex{} // 为 Cond 绑定锁
   return q
}

func (q *Queue) Put(item Item) {
   q.itemAdded.L.Lock()
   defer q.itemAdded.L.Unlock()
   q.items = append(q.items, item)
   q.itemAdded.Signal()        // 当 Queue 中加入数据成功,调用 Singal 发送通知
}

func (q *Queue) GetMany(n int) []Item {
   q.itemAdded.L.Lock()
   defer q.itemAdded.L.Unlock()
   for len(q.items) < n {     // 等待 Queue 中有 n 个数据
      q.itemAdded.Wait()      // 阻塞等待 Singal 发送通知
   }
   items := q.items[:n:n]
   q.items = q.items[n:]
   return items
}

func main() {
   q := NewQueue()

   var wg sync.WaitGroup
   for n := 10; n > 0; n-- {
      wg.Add(1)
      go func(n int) {
         items := q.GetMany(n)
         fmt.Printf("%2d: %2d\n", n, items)
         wg.Done()
      }(n)
   }

   for i := 0; i < 100; i++ {
      q.Put(i)
   }

   wg.Wait()
}

In this example, Queue is a structure that stores data Item , which controls the input and output of data Cond type itemAdded It can be noticed that 10 goroutines are used to consume data here, but the amount of data required by them is not equal, we can call it a batch, which is between 1-10. After that, gradually add 100 data to Queue . Finally, we can see that all 10 gotoutines can be awakened and get the data it wants.

The results of the program are as follows

 6: [ 7  8  9 10 11 12]
 5: [50 51 52 53 54]
 9: [14 15 16 17 18 19 20 21 22]
 1: [13]
 2: [33 34]
 4: [35 36 37 38]
 3: [39 40 41]
 7: [ 0  1  2  3  4  5  6]
 8: [42 43 44 45 46 47 48 49]
10: [23 24 25 26 27 28 29 30 31 32]

Of course, the results of the program will not be the same each time, the above output is only a certain case.

sync.Cond implementation

In $GOPATH/src/sync/cond.go , Cond is defined as follows

type Cond struct {
   noCopy noCopy
   L Locker
   notify  notifyList
   checker copyChecker
}

Among them, the noCopy and checker fields are used to prevent Cond from being copied during use. For details, see the "no copy mechanism" small chopper.

L is the Locker interface. Generally, the actual object of this field is *RWmutex or *Mutex .

type Locker interface {
   Lock()
   Unlock()
}

notifyList records a list of notifications based on ticket numbers. It doesn't matter if you don't understand the comments for the first time here, just read it back and forth.

type notifyList struct {
   wait   uint32         // 用于记录下一个等待者 waiter 的票号
   notify uint32         // 用于记录下一个应该被通知的 waiter 的票号
   lock   uintptr        // 内部锁
   head   unsafe.Pointer // 指向等待者 waiter 的队列队头
   tail   unsafe.Pointer // 指向等待者 waiter 的队列队尾
}

Among them, head and tail are pointers to the sudog structure, and sudog represents the goroutine in the waiting list, which itself is a doubly linked list. It is worth mentioning that there is a field ticket that is used to record the ticket number for the current goroutine.

The core mode implemented by Cond is ticket system (ticket system) . Every goroutine who wants to buy a ticket (calls Cond.Wait()) is called a waiter. The ticketing system will assign a ticket collection code to each waiter. , Waiter will be awakened when the ticket supplier has the number of the ticket collection code. There are two kinds of goroutines that sell tickets. The first is to call Cond.Signal(), which will wake up a waiter (if any) that buys a ticket according to the ticket number. The second is to call Cond.Broadcast(). It will notify all blocking waiters to wake up. In order to make it easier for readers to understand the ticketing system, we will give a graphical example below.

In the above, we know that the notifyList structure in the Cond field is a notification list that records ticket numbers. Here, notifyList is compared to queuing to get tickets to buy movie tickets. When G1 buys tickets through Wait, it finds that there are no tickets to buy at this time, so he can only block waiting for the notification after the ticket is available. At this time, he has already obtained the ticket. Exclusive ticket collection code 0. Similarly, G2 and G3 also have no tickets to buy, they got their own ticket codes 1 and 2 respectively. G4 is a movie ticket provider. It sells tickets. It has brought two tickets through two Signals, notified G1 and G2 in order of ticket numbers to pick up the tickets, and updated notify to the latest 1. G5 also bought a ticket. It found that there was no ticket to buy at this time, and took its own ticket code 3, and it blocked the waiting. G6 is a large ticket dealer. Through Broadcast, it can satisfy all the waiting ticket buyers to buy tickets. At this time, they are waiting for G3 and G5, so he directly awakens G3 and G5, and updates notify to be equal to the wait value. .

After understanding the operation principle of the above ticket collection system, let's look at the implementation of the four actual external method functions under the Cond package.

  • NewCond method
func NewCond(l Locker) *Cond {
   return &Cond{L: l}
}

Used to initialize the Cond object, that is, to initialize the control lock.

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

The runtime_notifyListAdd is implemented in the notifyListAdd of runtime/sema.go. It is used to atomically increase the waiter ticket number of the waiter and return the ticket number value t current goroutine should take. The implementation of runtime_notifyListWait is in the notifyListWait of runtime/sema.go. It will try to compare the current ticket numbers recorded in the t and notify If t less than the current ticket number, then it can be returned directly, otherwise it will wait for the number to be notified.

At the same time, it should be noted here that because when entering runtime_notifyListWait, the current goroutine is c.L.Unlock() through 061795b72c8997, which means that there may be multiple goroutines to change the conditions. Then, the current goroutine cannot guarantee that the condition must be true after runtime_notifyListWait returns, so it is necessary to loop to determine the condition . The correct Wait posture is as follows:

//    c.L.Lock()
//    for !condition() {
//        c.Wait()
//    }
//    ... make use of condition ...
//    c.L.Unlock()
  • Cond.Signal method
func (c *Cond) Signal() {   c.checker.check()   runtime_notifyListNotifyOne(&c.notify)}

The detailed implementation of runtime_notifyListNotifyOne is in the notifyListNotifyOne of runtime/sema.go. Its purpose is to notify the waiter to pick up the ticket. The specific operation is: if there is no new waiter to take the ticket after the last notification to take the ticket, then the function will return directly. Otherwise, it will get the ticket number +1 and notify the waiter who is waiting to get the ticket.

It should be noted that when calling the Signal method, you do not need to hold the cL lock.

  • Cond.Broadcast method
func (c *Cond) Broadcast() {   c.checker.check()   runtime_notifyListNotifyAll(&c.notify)}

The detailed implementation of runtime_notifyListNotifyAll is in the notifyListNotifyAll of runtime/sema.go. It will notify all waiters to wake up and notify value of wait be equal to the value of 061795b72c8a60. When calling the Broadcast method, there is no need to hold the cL lock.

discuss

Under $GOPATH/src/sync/cond.go , we can find that the amount of code is very small, but it only presents the core logic, and its implementation details are located in runtime/sema.go . relies on the scheduling primitive of the runtime layer. Readers who are interested in details can go deeper. Learn.

The question is, why do we rarely use sync.Cond in daily development?

  • Invalid wakeup

As we mentioned in the previous article, the correct posture using Cond.Wait is as follows

    c.L.Lock()    for !condition() {        c.Wait()    }    ... make use of condition ...    c.L.Unlock()

Taking the example at the beginning of the article, if you use the Broadcast method to wake up all the waiters each time the Put method is called, there is a high probability that the awakened waiter will wake up and find that the condition is not met, and will re-enter waiting. Although the Signal method is called to wake up the specified waiter, it does not guarantee that the conditions for the wake-up waiter will be met. Therefore, in actual use, we need to ensure that the wake-up operation is effective as much as possible. In order to achieve this, the complexity of the code will inevitably increase.

  • Hunger problem

Taking the example at the beginning of the article as an example, if there are multiple goroutines executing GetMany(3) and GetMany(3000) at the same time, the probability that the goroutine executing GetMany(3) and executing GetMany(3000) will be awakened is the same, but because GetMany( 3) Only 3 pieces of data are required to meet the condition. If the goroutine of GetMany(3) always exists, the goroutine that executes GetMany(3000) will never get the data and will always be invalidly awakened.

  • Cannot respond to other events

The meaning of the condition variable is to let the goroutine go to sleep when a certain condition occurs. But this will cause goroutine to miss some other events that need attention while waiting for the condition. For example, the function that calls Cond.Wait contains context. When the context sends a cancellation signal, it cannot get the cancellation signal and exit as we expect. The use of Cond prevents us from selecting conditions and other events at the same time.

  • Substitution

Through the analysis of several external methods of sync.Cond, it is not difficult to see that its usage scenarios can be replaced by channels, but this will also increase the complexity of the code. The above example can be rewritten as follows using channel.

type Item = inttype waiter struct {    n int    c chan []Item}type state struct {    items []Item    wait  []waiter}type Queue struct {    s chan state}func NewQueue() *Queue {    s := make(chan state, 1)    s <- state{}    return &Queue{s}}func (q *Queue) Put(item Item) {    s := <-q.s    s.items = append(s.items, item)    for len(s.wait) > 0 {        w := s.wait[0]        if len(s.items) < w.n {            break        }        w.c <- s.items[:w.n:w.n]        s.items = s.items[w.n:]        s.wait = s.wait[1:]    }    q.s <- s}func (q *Queue) GetMany(n int) []Item {    s := <-q.s    if len(s.wait) == 0 && len(s.items) >= n {        items := s.items[:n:n]        s.items = s.items[n:]        q.s <- s        return items    }    c := make(chan []Item)    s.wait = append(s.wait, waiter{n, c})    q.s <- s    return <-c}

Finally, although the potential problems of sync.Cond are listed in the above discussion, if the developer can consider the above issues in use, for the implementation of the monitor model, in the semantic logic of the code Above, the use of sync.Cond will be easier to understand and maintain than the channel mode. Remember, easy-to-understand code models are always more down-to-earth than esoteric dazzling skills.


机器铃砍菜刀
125 声望63 粉丝