实现同步的几种方式
- 互斥锁
- 条件变量
- 原子操作
- chanel
- sycn.WaitGroup & sync.Once
Channel 与 sync.WaitGroup & sync.Once 的区别
Channel 实现同步的方法
声明一个通道,使得它的数量与我们手动启动 goroutine 的数量相同,之后在利用这个通道使得主 goroutine 等待其他 goroutine 执行结束
func coordinateWithChan() {
sign := make(chan struct{}, 2) // 实现声明channel,使得它的数量与手动启动goroutine的数量相同
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(结构体类型) 的实现同步方法
sync 包的 WaitGroup 比 channel 更适合一对多的 goroutine 的协作流程
WaitGroup 拥有三个指针方法:
- Add (WaitGroup内有一个计数器,默认值是0,Add方法可以添加计算器的值)
- Done (每调用一个该方法,计数器都会自动减1)
- Wait (若WaitGroup内部计数器不为0,调用Wait方法都会阻塞goroutine的执行)
func coordinateWithGroup() {
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 需要注意的几个问题
WaitGroup类型值中计数器的值不可以小于0
-
WaitGroup值是可以被复用的,但需要保证其计数周期的完整性
一个计数周期的过程:WaitGroup 计数器的值由0变成一个正整数,然后经过一系列过程变成0
- Wait 方法在某个计数周期被调用就立刻阻塞当前的goroutine,直至计数周期完成
- 一个Wait方法只能归属于一个计数周期,若Wait方法跨计数周期就会引发panic
- 不要把增加计数器的add方法和wait方法放到不同的goroutine中,否则会引发panic
sync.Once
Once 类型(结构体类型)的 Do 方法只接受一个参数
- 这个参数类型必须是func(),即无参数声明、无结果声明的函数
- 该方法并不是对每个参数函数都执行一次,而是只执行“首次调用时”传入的函数,之后不会执行任何参数函数
- 如果有多个执行一次的函数,那么就应该为它们分别分配一个 sync.Once 类型的值
func main() {
var counter uint32
var once sync.Once
// 下面两个once.Do只会执行一次
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: %d\n", counter)
fmt.Println()
}
- sync.Once 类型的变量可多次重新复制新的该类型的值
package main
import (
"fmt"
"errors"
"time"
"sync/atomic"
"sync"
)
func main() {
// Demo1
fmt.Println("Demo1")
var counter uint32
var once sync.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: %d\n", counter)
fmt.Println()
// Demo2
fmt.Println("Demo2")
once = sync.Once{} // 再次为sync.Once类型变量重新复制
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
once.Do(func() {
for i := 0; i < 3; i ++ {
fmt.Printf("Do task. [1-%d]\n", i)
time.Sleep(time.Second)
}
})
fmt.Println("Done. [1]")
}()
go func() {
defer wg.Done()
time.Sleep(time.Microsecond * 500)
once.Do(func() {
fmt.Println("Do task. [2]")
})
fmt.Println("Done. [2]")
}()
go func() {
defer wg.Done()
time.Sleep(time.Microsecond * 500)
once.Do(func() {
fmt.Println("Do task. [3]")
})
fmt.Println("Done. [3]")
}()
wg.Wait()
}
sync.Once类型值的Do方法是怎么保证只执行参数函数一次
- 这个结构体类型中包含一个 sync.Mutex 类型的字段(建议不要在多个函数相互传递该值)
这个结构体中包含一个uint32类型的done字段,默认是0一旦有Do方法调用完成done字段的值变成1
为什么done字段的类型是uint32类型?
- 原因很简单,因为对它的操作必须是“原子”的
- Do方法在一开始就会通过调用atomic.LoadUint32函数来获取该字段的值
- 一旦发现该值为1,就会直接返回
- 多个goroutine同时调用Do方法,还会使用 sync.Mutex 在临界区再次检查done字段是否为1,若为false调用原子操作将done字段改为1
- Do方法在最后将done改为1的操作是放在defer语句中,因此Do方法内的函数参数无论以什么方式结束都会将done字段改成1
sync.WaitGroup 与 sync.once 的区别
- WaitGroup 只使用到原子操作
- sync.once 不但使用原子操作还使用互斥锁
- 都是开箱即用的,也是并发安全的