• Golang 协程同步机制详解


    本文介绍协同同步机制,基于示例和源码详解其实现原理,并总结应用同步机制的最佳实践。

    问题引入

    为了等待协程完成,我们可以使用空结构体通道,并在操作最后给通道发送值。

    	ch := make(chan struct{})
    	for i := 0; i < n; n++ {
    		go func() {
    			// do something
    			ch <- struct{}{}
    		}()
    	}
    	for i := 0; i < n; n++ {
    		<-ch
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这种策略可以实现,但不建议使用。这在语义上也不正确,使用通道作为工具发送空数据,我们的使用场景是同步而不是通信。这就需要引入sync.WaitGroup 数据结构,专门用于同步场景。

    同步机制

    sync.WaitGroup 数据结构包括主状态,称为计数器,标识等待元素数量,源码如下:

    // A WaitGroup waits for a collection of goroutines to finish.
    // The main goroutine calls Add to set the number of
    // goroutines to wait for. Then each of the goroutines
    // runs and calls Done when finished. At the same time,
    // Wait can be used to block until all goroutines have finished.
    //
    // A WaitGroup must not be copied after first use.
    //
    // In the terminology of the Go memory model, a call to Done
    // “synchronizes before” the return of any Wait call that it unblocks.
    type WaitGroup struct {
    	noCopy noCopy
    
    	// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    	// 64-bit atomic operations require 64-bit alignment, but 32-bit
    	// compilers only guarantee that 64-bit fields are 32-bit aligned.
    	// For this reason on 32 bit architectures we need to check in state()
    	// if state1 is aligned or not, and dynamically "swap" the field order if
    	// needed.
    	state1 uint64
    	state2 uint32
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    noCopy字段防止按值拷贝,还有状态字段。另外还提供三个方法:

    • Add

    使用指定值改变计数器的值,也可以是负数。如果计数器变为零,应用会panics.

    • Done
    // Done decrements the WaitGroup counter by one.
    func (wg *WaitGroup) Done() {
    	wg.Add(-1)
    }
    
    • 1
    • 2
    • 3
    • 4

    可以但Done是Add(-1)的简化形式。通常在协程完成工作时调用。

    • Wait

    该操作阻塞当前协程直到计数器为零。

    案例分析

    下面使用WaitGroup重构上面示例,会让代码更清晰、易读:

    	func main() {
    		wg := sync.WaitGroup{}
    		wg.Add(10)
    		for i := 1; i <= 10; i++ {
    			go func(a int) {
    				for i := 1; i <= 10; i++ {
    					fmt.Printf("%dx%d=%d\n", a, i, a*i)
    				}
    				wg.Done()
    			}(i)
    		}
    		wg.Wait()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    循环开始前,我们设置WaitGroup计数器为协程的数量,这我们在启动之前就已知道。然后每完成一个使用Done方法减少计数器。

    如果事前不知道总数量呢,Add方法可以在开始执行协程之前增加1,代码如下:

    func main() {
    	wg := sync.WaitGroup{}
    	for i := 1; rand.Intn(10) != 0; i++ {
    		wg.Add(1)
    		go func(a int) {
    			for i := 1; i <= 10; i++ {
    				fmt.Printf("%dx%d=%d\n", a, i, a*i)
    			}
    			wg.Done()
    		}(i)
    	}
    	wg.Wait()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面示例事前不任务数量不确定,因此在每次任务之前调用Add(1),其他过程与上面一致。

    常见的错误是add方法在协程内部,这通常会导致提前退出,而不执行任何gor协程。

    引用源码中的注释作为最佳实践:

    A WaitGroup waits for a collection of goroutines to finish.
    
    The main goroutine calls Add to set the number of goroutines to wait for. 
    Then each of the goroutines runs and calls Done when finished. 
    At the same time, Wait can be used to block until all goroutines have finished.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    简单翻译下:

    WaitGroup实现等待一组协程完成机制。

    • 主协程调用Add方法,并使之需要等待协程的数量
    • 每个协程运行,结束时调用Done方法
    • 同时,Wait方法阻塞,直到所有协程都完成
  • 相关阅读:
    LCR 123.图书整理
    如何对服务端进行性能测试
    系统优化脚本支持Ubuntu和CentOS
    ROS Turtlebot3多机器人编队导航仿真
    pandas教程:Apply:General split-apply-combine 通常的分割-应用-合并
    车规级MCU进入「新周期」,中国本土供应商竞逐竞争力TOP10
    逆向分析-SeparationPreview.aip-分色预览(二)-定位checkbox点击代码位置
    C++中如何使用通用字符名输入UNICODE字符
    写给 Java 程序员的前端 Promise 教程
    ZYNQ7000交叉编译MPlayer到开发板播放视频
  • 原文地址:https://blog.csdn.net/neweastsun/article/details/127939849