并发编程-Sync包
sync.WaitGroup
在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 计数器+delta |
(wg *WaitGroup) Done() | 计数器-1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
我们利用sync.WaitGroup将上面的代码优化一下:
go
var wg sync.WaitGroup
func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}
需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。
sync.Once
sync.Once
是 Go 语言 sync
包提供的一个同步原语,用于确保某个操作只执行一次,无论它被调用多少次或有多少个 Goroutine 同时尝试执行它。
它的主要用途包括:
1.延迟初始化 (Lazy Initialization) :当你想在资源第一次被需要时才进行初始化,而不是在程序启动时就初始化,sync.Once
可以确保初始化只发生一次,从而避免重复初始化的开销。
go
import (
"fmt"
"sync"
"time"
)
var once sync.Once
var expensiveResource string
func getExpensiveResource() string {
once.Do(func() {
fmt.Println("正在初始化昂贵资源......")
time.Sleep(100 * time.Millisecond) // 模拟初始化耗时
expensiveResource = "这是一个昂贵的资源"
})
return expensiveResource
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(getExpensiveResource())
}()
}
wg.Wait()
fmt.Println("所有 Goroutine 已完成。")
}
在上面的例子中,"正在初始化昂贵资源..."
只会被打印一次,即使 getExpensiveResource
被多个 Goroutine 调用。

2.单例模式 (Singleton Pattern):确保一个类的实例只被创建一次。
go
package main
import (
"fmt"
"sync"
)
type Singleton struct {
Name string
}
var (
instance *Singleton
once sync.Once
)
func GetSingletonInstance() *Singleton {
once.Do(func() {
fmt.Println("create Singleton instance")
instance = &Singleton{Name: "my Singleton instance"}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s := GetSingletonInstance()
fmt.Println(s.Name)
}()
}
wg.Wait()
}

3.包级别初始化 (Package-Level Initialization):在包加载时进行一次性设置。
sync.Once
的新特性 (Go 1.21 及更高版本):
Go 1.21 引入了 OnceFunc
, OnceValue
, 和 OnceValues
,它们是 sync.Once
的封装,提供了更简洁的用法:
OnceFunc(f func()) func()
: 返回一个函数,该函数只会调用f
一次。多次调用返回的函数都会执行,但f
只会在第一次被调用。OnceValue[T any](f func() T) func() T
: 返回一个函数,该函数只会调用f
一次并缓存f
的返回值。后续调用都会返回相同的值。OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
: 类似于OnceValue
,但用于返回两个值。
示例:OnceValue
go
package main
import (
"fmt"
"sync"
)
func main() {
// 定义一个只计算一次并返回结果的函数
getSumOnce := sync.OnceValue(func() int {
sum := 0
for i := 0; i < 1000; i++ {
sum += i
}
fmt.Println("计算了一次:", sum)
return sum
})
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result := getSumOnce() // 这里开辟10个协程 执行函数,但实际只执行了一次
fmt.Printf("Goroutine %d 得到结果: %d\n", i, result)
}()
}
wg.Wait()
}
在这个例子中,getSumOnce
函数只会在第一次被调用时执行内部的求和逻辑,并打印 "计算了一次:",之后的调用会直接返回缓存的结果。

重要注意事项:
sync.Once
实例一旦执行过其Do
方法,就不能被重置。- 如果传递给
Do
方法的函数发生 panic,sync.Once
仍会认为该操作已完成,后续调用不会再次执行该函数。 sync.Once
不应被复制。
总而言之,sync.Once
是 Go 中一个强大且常用的同步工具,用于安全地执行一次性初始化。
sync.Map
sync.Map
是 Go 语言 sync
包提供的一个并发安全的哈希映射(map),它旨在解决 Go 内置 map
在并发访问时需要外部 sync.RWMutex
保护的性能问题,尤其是在读多写少的场景下。
Go 语言内置的 map
不是并发安全的。如果在多个 Goroutine 中同时对一个 map
进行读写操作,会导致数据竞争(data race),进而引发程序崩溃或产生不确定的行为。为了解决这个问题,通常需要使用 sync.RWMutex
来保护 map
的并发访问
go
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。
像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map--sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法
sync.Map 的常用方法:
-
Store(key, value interface{}): 将 key-value 对存储到 Map 中。
-
Load(key interface{}) (value interface{}, ok bool): 根据 key 加载值。如果 key 存在,返回 value 和 true;否则返回 nil 和 false。
-
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool): 如果 key 存在,则返回其现有值和 true;否则,存储新的 key-value 对并返回 value 和 false。这是一个非常常用的方法,可以实现"如果不存在就存储,否则就加载"的逻辑。
-
Delete(key interface{}): 从 Map 中删除 key 及其对应的值。
-
Range(f func(key, value interface{}) bool): 遍历 Map 中的所有 key-value 对,对每个对调用提供的函数 f。如果 f 返回 false,则停止遍历。注意:在 Range 期间,Map 可能会被并发修改,所以遍历的结果可能不是一个完全一致的快照。
go
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
