golang之sync包
介绍sync包中常用的方法,
- sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和 工具类(Once、WaitGroup、Cond、Pool、Map)
- sync/atomic:提供变量的原子操作(基于硬件指令 compare-and-swap)
[Mutex] 互斥锁
Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。互斥锁 Mutex
类型有两个方法,Lock
和 Unlock
。
使用互斥锁的注意事项:
- Mutex 类型变量的零值是一个未锁定状态的互斥锁。
- Mutex 在首次被使用之后就不能再被拷贝(Mutex 是值类型,拷贝会同时拷贝互斥锁的状态)。
- Mutex 在未锁定状态(还未锁定或已被解锁),调用
Unlock
方法,将会引发运行时错误。 - Mutex 的锁定状态与特定 goroutine 没有关联,Mutex 被一个 goroutine 锁定, 可以被另外一个 goroutine 解锁。(不建议使用,必须使用时需要格外小心。)
- Mutex 的
Lock
方法和Unlock
方法要成对使用,不要忘记将锁定的互斥锁解锁,一般做法是使用 defer。
源码:
type Mutex struct { state int32 // 互斥锁的状态 sema uint32 // 信号量,用于控制互斥锁的状态 }
# 求和运算 func mutexAdd() { var total int32 // 使用多协程处理 var wg sync.WaitGroup var lock sync.Mutex for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() // 不加锁的情况下,会出现total不为100的时候 lock.Lock()
defer lock.Unlock() // 防止忘记解锁 total += 1 //atomic.AddInt32(&total, 1) // 无需加锁即可 // lock.Unlock() }() } wg.Wait() fmt.Println("total:", total) }
[RWLock] 读写锁
RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。读写互斥锁 RWMutex
类型有五个方法,Lock
,Unlock
,Rlock
,RUnlock
和 RLocker
。其中,RLocker 返回一个 Locker 接口,该接口通过调用 rw.RLock
和 rw.RUnlock
来实现 Lock 和 Unlock 方法。
使用读写互斥锁的注意事项:
- RWMutex 类型变量的零值是一个未锁定状态的互斥锁。
- RWMutex 在首次被使用之后就不能再被拷贝。
- RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。
- RWMutex 的一个写锁
Lock
去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁。 - RWMutex 的读锁不要用于递归调用,比较容易产生死锁。
- RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。
- 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁。
- 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 goroutine,其中等待时间最长的一个 goroutine 会被唤醒。
读写锁的访问控制规则如下:
① 多个写操作之间是互斥的
② 写操作与读操作之间也是互斥的
③ 多个读操作之间不是互斥的
源码:
type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers readerWait int32 // number of departing readers }
使用示例:
package main import "sync" type Cache struct { data map[string]any rwMutex sync.RWMutex } func NewCache() *Cache { return &Cache{ data: make(map[string]any), } } func (c *Cache) GetValue(key string) (any, bool) { c.rwMutex.RLock() defer c.rwMutex.RUnlock() val, isOk := c.data[key] return val, isOk } func (c *Cache) SetValue(key string, val any) { c.rwMutex.Lock() // 对于写入操作使用互斥锁 defer c.rwMutex.Unlock() c.data[key] = val }
参考文档: https://juejin.cn/post/7218554163051413561
[Once]
sync.Once 被用于控制变量的初始化,这个变量的读写通常遵循单例模式,满足这三个条件:
- 当且仅当第一次读某个变量时,进行初始化(写操作)
- 变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化完成后,读操作继续进行
- 变量仅初始化一次,初始化完成后驻留在内存里
once.Sync
可用于任何符合 "exactly once" 语义的场景,比如:
- 初始化 rpc/http client
- open/close 文件
- close channel
- 线程池初始化
[WaitGroup]
一个 WaitGroup 对象可以等待一组协程结束
。使用方法是:
- main协程通过调用
wg.Add(delta int)
设置worker协程的个数,然后创建worker协程; - worker协程执行结束以后,都要调用
wg.Done()
; - main协程调用
wg.Wait()
且被block,直到所有worker协程全部执行结束后返回。
实例:
func main() { // 省略部分代码 ... var wg sync.WaitGroup for _, task := range tasks { task := task wg.Add(1) go func() { task() defer wg.Done() }() } wg.Wait() // 省略部分代码... }
说明:
1.wg.Done 必须在wg.Add之后执行
2.wg.Done在worker协程中调用, 保证调用一次,不能因为panic或者其他原因导致没有执行(建议使用defer wg.Done)
3.task := task 需要进行再次赋值,否则会读取到最后一个元素的值(
- for-loop 内创建的局部变量,即便名字相同,内存地址也不会复用
)
适用场景:
1.需要向多个服务请求数据,并最终将这些服务请求结果进行最终整合返回给前端进行展示
基于WaitGroup的使用,发现一个协程出现错误的时候,并不会将子协程的错误抛送给主goroutine : golang.org/x/sync/errgroup
使用示例:
package main import ( "fmt" "net/http" "golang.org/x/sync/errgroup" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.baidu.com/", "http://www.bokeyuan12111.com/", } g := new(errgroup.Group) for _, url := range urls { url := url g.Go(func() error { resp, err := http.Get(url) if err != nil { fmt.Println(err) return err } fmt.Printf("get [%s] success: [%d] \n", url, resp.StatusCode) return resp.Body.Close() }) } if err := g.Wait(); err != nil { fmt.Println(err) } else { fmt.Println("All success!") } }
输出:
get [http://www.baidu.com/] success: [200]
Get "http://www.bokeyuan12111.com/": dial tcp: lookup www.bokeyuan12111.com: no such host
Get "http://www.golang.org/": dial tcp 142.251.42.241:80: i/o timeout
Get "http://www.bokeyuan12111.com/": dial tcp: lookup www.bokeyuan12111.com: no such host
可以看到,执行获取www.bokeyuan12111.com和www.golang.org两个 url 的子 groutine 均发生了错误,在主任务 goroutine 中成功捕获到了第一个错误信息。
除了 拥有 WaitGroup 的控制能力 和 错误传播 的功能之外,errgroup 还有最重要的 context 反向传播机制
带有上下文取消的使用方式:
package main import ( "context" "fmt" "golang.org/x/sync/errgroup" ) func main() { g, ctx := errgroup.WithContext(context.Background()) dataChan := make(chan int, 20) // 数据生产端任务子 goroutine g.Go(func() error { defer close(dataChan) for i := 1; ; i++ { if i == 10 { return fmt.Errorf("data 10 is wrong") } dataChan <- i fmt.Println(fmt.Sprintf("sending %d", i)) } }) // 数据消费端任务子 goroutine for i := 0; i < 3; i++ { g.Go(func() error { for j := 1; ; j++ { select { case <-ctx.Done(): return ctx.Err() case number := <-dataChan: fmt.Println(fmt.Sprintf("receiving %d", number)) } } }) } // 主任务 goroutine 等待 pipeline 结束数据流 err := g.Wait() if err != nil { fmt.Println(err) } fmt.Println("main goroutine done!") }
在以上示例中,我们模拟了一个数据传送管道。在数据的生产与消费任务集中,有四个子任务 goroutine:一个生产数据的 goroutine,三个消费数据的 goroutine。当数据生产方存在错误数据时(数据等于 10 ),我们停止数据的生产与消费,并将错误抛出,回到 main goroutine 的执行逻辑中。
可以看到,因为 errgroup 中的 Context cancle 函数的嵌入,我们在子任务 goroutine 中也能反向控制任务上下文。
程序的某一次运行,输出结果如下:
sending 1 sending 2 sending 3 sending 4 sending 5 sending 6 sending 7 sending 8 sending 9 receiving 1 receiving 3 receiving 2 receiving 4 data 10 is wrong main goroutine done!
[Cond]
[Pool]
[Map]
[sync/atomic] 原子相关操作