实现同步的几种方式
- 互斥锁
- 条件变量
- 原子操作
- 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.WaitGroupwg.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 uint32var 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 mainimport ("fmt""errors""time""sync/atomic""sync")func main() {// Demo1fmt.Println("Demo1")var counter uint32var 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()// Demo2fmt.Println("Demo2")once = sync.Once{} // 再次为sync.Once类型变量重新复制var wg sync.WaitGroupwg.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 不但使用原子操作还使用互斥锁
- 都是开箱即用的,也是并发安全的
