GoLang的sync.WaitGroup与sync.Once简单使用讲解
鲲鹏飞九万里 人气:0一、sync.WaitGroup的简单实用
在之前,我们使用通道,来主goroutine中等待其他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 }
其实,可以用更简单的方法,使用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() }
sync包的WaitGroup类型。它比通道更加适合实现这种一对多的 goroutine 协作流程。
sync.WaitGroup
类型(以下简称WaitGroup类型)是开箱即用的,也是并发安全的。同时,它一旦被真正使用就不能被复制了。
WaitGroup类型拥有三个指针方法:Add、Done和Wait。
Add方法
可以想象该类型中有一个计数器,它的默认值是0。我们可以通过调用该类型值的Add方法来增加,或者减少这个计数器的值。
Done方法
用这个方法来记录需要等待的 goroutine 的数量。相对应的,这个类型的Done方法,用于对其所属值中计数器的值进行减一操作。我们可以在需要等待的 goroutine 中,通过defer语句调用它。
Wait方法
此类型的Wait方法的功能是,阻塞当前的 goroutine,直到其所属值中的计数器归零。如果在该方法被调用的时候,那个计数器的值就是0,那么它将不会做任何事情。
二、sync.WaitGroup类型值中计数器的值可以小于0吗
不可以。
之所以说WaitGroup值中计数器的值不能小于0,是因为这样会引发一个 panic。 不适当地调用这类值的Done方法和Add方法都会如此。
- 虽然WaitGroup值本身并不需要初始化,但是尽早地增加其计数器的值,还是非常有必要的。
- WaitGroup值是可以被复用的,但需要保证其计数周期的完整性。
- 不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行。换句话说,要杜绝对同一个WaitGroup值的两种操作的并发执行。
三、sync.Once
sync.Once
也属于结构体类型,同样也是开箱即用和并发安全的。由于这个类型包含了一个sync.Mutex
类型的字段,所以,复制该类型的值也会导致功能的失效。
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 }
用法
Once
类型的Do
方法只接受一个参数,这个参数的类型必须是func()
,即无参数声明和结果声明的函数。
该方法的功能并不是对每一种参数函数都只执行一次,而是只执行“首次被调用时传入的”那个函数,并且之后不会再执行任何参数函数。
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
类型的值。
sync.Once类型中的uint32类型的字段
sync.Once
类型中有一个名叫done
的uint32类型的字段。它的作用是记录其所属值的Do方法被调用的次数。该字段的值只可能为0或1。
一旦Do方法首次调用完成,它的值就会从0变为1。
使用uint32 类型是为了保证原子性。
修改done
,使用了“双重判断+锁”的方式,类似于GoF设计模式中的单例模式。
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() } }
Do方法的功能特点
第一个特点:于Do方法只会在参数函数执行结束之后把done字段的值变为1,因此,如果参数函数的执行需要很长时间或者根本就不会结束(比如执行一些守护任务),那么就有可能会导致相关 goroutine 的同时阻塞。
第二个特点:Do方法在参数函数执行结束后,对done字段的赋值用的是原子操作,并且,这一操作是被挂在defer语句中的。因此,不论参数函数的执行会以怎样的方式结束,done字段的值都会变为1。
也就是说,即使这个参数函数没有执行成功(比如引发了一个 panic),我们也无法使用同一个Once值重新执行它了。所以,如果你需要为参数函数的执行设定重试机制,那么就要考虑Once值的适时替换问题。
加载全部内容