GO's sync.WaitGroup and sync.Once
Article directory
1. sync.WaitGroup
Simple and practical
Before, we used channels to wait for other goroutines to complete in the main goroutine:
func coordinateWithChan() {
sign := make(chan struct{
}, 2)
num := int32(0)
fmt.Printf("The number: %d [with chan struct{}]\n", num)
max := int32(10)
go addNum(&num, 1, max, func() {
sign <- struct{
}{
}
})
go addNum(&num, 2, max, func() {
sign <- struct{
}{
}
})
<-sign
<-sign
}
Actually, you can do it in a simpler way, using sync.WaitGroup
:
func coordinateWithWaitGroup() {
var wg sync.WaitGroup
wg.Add(2)
num := int32(0)
fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
max := int32(10)
go addNum(&num, 3, max, wg.Done)
go addNum(&num, 4, max, wg.Done)
wg.Wait()
}
The WaitGroup type of the sync package. It is more suitable for implementing this one-to-many goroutine coordination process than channels.
sync.WaitGroup
type (hereinafter referred to as the WaitGroup type) is out of the box and is also concurrency safe. At the same time, once it is actually used, it cannot be copied.
The WaitGroup type has three pointer methods: Add, Done, and Wait.
(1) Add method
It is conceivable that there is a counter in this type, and its default value is 0. We can increment or decrement the value of this counter by calling the Add method of this type of value.
(2) Done method
Use this method to keep track of the number of goroutines that need to wait. Correspondingly, the Done method of this type is used to decrement the value of the counter in its own value by one. We can call it through the defer statement in the goroutine that needs to wait.
(3) Wait method
The function of this type of Wait method is to block the current goroutine until the counter in its own value is zeroed. If the value of that counter is 0 when this method is called, then it will do nothing.
2. Can the value of the counter in the sync.WaitGroup type value be less than 0?
Can't.
The reason why the value of the counter in the WaitGroup value cannot be less than 0 is because this will cause a panic. This is true for both the Done method and the Add method that inappropriately call such values.
- Although the WaitGroup value itself does not need to be initialized, it is still very necessary to increase the value of its counter as soon as possible.
- The WaitGroup value can be reused, but the integrity of its counting cycle needs to be guaranteed.
- Do not put the operation of increasing its counter value and the code of calling its Wait method in different goroutines. In other words, it is necessary to prevent the concurrent execution of two operations on the same WaitGroup value.
three,sync.Once
sync.Once
Also belongs to the structure type, which is also out of the box and concurrency safe. Since this type contains a sync.Mutex
type field, copying the value of this type will also cause the function to fail.
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
(1) Usage
Once
A method of type Do
accepts only one parameter, and the type of this parameter must be func()
, that is, a function with no parameter declaration and result declaration.
The function of this method is not to execute each parameter function only once, but to only execute the function "passed in when it is called for the first time", and will not execute any parameter functions afterwards.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter uint32
var once sync.Once
once.Do(func() {
atomic.AddUint32(&counter, 1)
})
fmt.Printf("The counter: %d\n", counter)
once.Do(func() {
atomic.AddUint32(&counter, 2)
})
fmt.Printf("The counter: %v\n", counter)
fmt.Println()
}
$ go run demo02.go
The counter: 1
The counter: 1
$
sync.Once
So, if you have multiple functions that only need to be executed once, you should assign a value of a type to each of them .
(2) sync.Once
Fields of type uint32 in type
sync.Once
The type has a done
field named uint32. Its role is to record the number of times the Do method of its value is called. The value of this field can only be 0 or 1.
Once the Do method is first called, its value changes from 0 to 1.
The uint32 type is used to guarantee atomicity.
The modification done
uses the "double judgment + lock" method, which is similar to the singleton pattern in the GoF design pattern.
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
(3) Functional characteristics of the Do method
The first feature: the Do method will only change the value of the done field to 1 after the execution of the parameter function is completed. Therefore, if the execution of the parameter function takes a long time or will not end at all (such as performing some guardian tasks), Then it may cause simultaneous blocking of related goroutines.
The second feature: after the execution of the parameter function, the Do method uses an atomic operation to assign the done field, and this operation is hung in the defer statement. Therefore, no matter how the execution of the parameter function will end, the value of the done field will become 1.
That is to say, even if this parameter function is not successfully executed (for example, a panic is triggered), we cannot re-execute it with the same Once value. Therefore, if you need to set a retry mechanism for the execution of the parameter function, then you must consider the timely replacement of the Once value.