1. Sync

    在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

    sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

    我们利用sync.WaitGroup将上面的代码优化一下:

    需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。

    说在前面的话:这是一个进阶知识点。

    Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。

    sync.Once只有一个Do方法,其签名如下:

    注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

    加载配置文件示例

    延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:

    多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:

    1. func loadIcons() {
    2. icons = make(map[string]image.Image)
    3. icons["left"] = loadIcon("left.png")
    4. icons["up"] = loadIcon("up.png")
    5. icons["right"] = loadIcon("right.png")
    6. icons["down"] = loadIcon("down.png")

    使用sync.Once改造的示例代码如下:

    sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

    Go语言中内置的map不是并发安全的。请看下面的示例:

    1. var m = make(map[string]int)
    2. return m[key]
    3. }
    4. func set(key string, value int) {
    5. m[key] = value
    6. }
    7. func main() {
    8. wg.Add(1)
    9. go func(n int) {
    10. key := strconv.Itoa(n)
    11. set(key, n)
    12. fmt.Printf("k=:%v,v:=%v\n", key, get(key))
    13. wg.Done()
    14. }(i)
    15. }
    16. wg.Wait()

    上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

    像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。