Go sync并发工具包
简介
在Java中提供Sychronized
关键字提供独占锁,Lock
类提供读写锁。在sync包中实现的功能也是与锁相关,包中主要包含的有:
- sync.Map:并发安全 map
- sync.Mutex:锁
- sync.RWMutex:读写锁
- sync.Once:只执行一次
- sync.WaitGroup: goroutine 之间同步
- sync.Pool:复用缓存池
sync.Map
key 和 value 类型都是 Any
。意味着你要搞各 种类型断言
使用示例
package main
import (
"fmt"
"sync"
)
func main() {
m := sync.Map{}
m.Store("cat", "Tom")
m.Store("mouse", "Jerry")
// 这里重新读取出来的,就是
val, ok := m.Load("cat")
if ok {
fmt.Println(len(val.(string)))
fmt.Printf("%s \n", val)
}
}
sync.Mutex 和sync.RWMutex
使用案例
package main
import (
"sync"
)
var mutex sync.Mutex
var rwMutex sync.RWMutex
func Mutex() {
mutex.Lock()
defer mutex.Unlock()
// 你的代码
}
func RwMutex() {
// 加读锁
rwMutex.RLock()
defer rwMutex.RUnlock()
// 也可以加写锁
rwMutex.Lock()
defer rwMutex.Unlock()
}
// 不可重入例子
func Failed1() {
mutex.Lock()
defer mutex.Unlock()
// 这一句会死锁
// 但是如果你只有一个goroutine,那么这一个会导致程序崩溃
mutex.Lock()
defer mutex.Unlock()
}
// 不可升级
func Failed2() {
rwMutex.RLock()
defer rwMutex.RUnlock()
// 这一句会死锁
// 但是如果你只有一个goroutine,那么这一个会导致程序崩溃
mutex.Lock()
defer mutex.Unlock()
}
mutex家族注意事项
- 尽量用 RWMutext
- 尽量用 defer 来释放锁,防止panic没有释放锁
- 不可重入:lock 之后,即便是同一个线程(goroutine),也无法再次加锁(写递归函数要小心)
- 不可升级:加了读锁之后,如果试图加写锁,锁不升级
不可重入和不可升级,和很多语言的实现都是不同 的,因此要小心使用
sync.Once
package main
import (
"fmt"
"sync"
)
func main() {
PrintOnce()
PrintOnce()
PrintOnce()
}
var once sync.Once
// 这个方法,不管调用几次,只会输出一次
func PrintOnce() {
once.Do(func() {
fmt.Println("只输出一次")
})
}
sync.WaitGroup
WaitGroup,它有3个函数:
- Add():在被等待的协程启动前加1,代表要等待1个协程。
- Done():被等待的协程执行Done,代表该协程已经完成任务,通知等待协程。
- Wait(): 等待其他协程的协程,使用Wait进行等待。
type WaitGroup
func (wg *WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}
下怎么用WaitGroup实现上面的问题。
队长先创建一个WaitGroup对象wg,
每个队员都是1个协程, 队长让队员出发前,使用wg.Add(),
队员出发寻找钥匙,队长使用wg.Wait()等待(阻塞)所有队员完成,
某个队员完成时执行wg.Done(),等所有队员找到钥匙,
wg.Wait()则返回,完成了等待的过程,接下来就是开箱。
package main
import (
"fmt"
"sync"
)
func main() {
// 钥匙
key := 0
// 资本家队长创建队伍
wg := sync.WaitGroup{}
// 这个队伍十个人
wg.Add(10)
for i := 1; i <= 10; i++ {
go func(val int) {
key = val
// 打工人队员找到了钥匙
wg.Done()
}(i)
}
// 队长等待队伍集合,拿回钥匙,如果不等待,钥匙数量不定
// 把这个注释掉你会发现,钥匙数量不知道会有多少个
wg.Wait()
fmt.Println(key)
}
sync.Pool
pool是什么
Golang在 1.3 版本的时候,在sync包中加入一个新特性:Pool。 简单的说:就是一个临时对象池。
为什么需要sync.pool
保存和复用临时对象,减少内存分配,降低GC压力
对象越多GC越慢,因为Golang进行三色标记回收的时候,要标记的也越多,自然就慢了
sync.pool 特性
- 优先从自己的缓存里面返回数据
- 如果不够了,就创建一个新的,使用传入的 New 回调
- 在 GC 的时候,sync.Pool 会被清空
sync.pool 缺陷
- 没有超时机制
- 无法限制内存使用量
- GC 全部清掉
使用案例
sync.Pool 的一般使用步骤(以user为例)
- 定义 user 结构体
- 为 user 加上 Reset 方法。该方法用于重置对象, 接收器是指针
- 定义一个 sync.Pool
- 调用 Get 方法
- 使用defer 保证放回去 pool
- 重置 user
- 执行业务逻辑
package main
import (
"fmt"
"sync"
)
func main() {
// 初始化一个pool
pool := sync.Pool{
// 默认的返回值设置,不写这个参数,默认是nil
New: func() interface{} {
return &user{}
}}
// Get 返回的是 interface{},所以需要类型断言
// Get 方法会返回 Pool 已经存在的对象;如果没有就使用New方法创建.
u := pool.Get().(*user)
// 对象或资源不用时,调用 Put 方法把对象或资源放回池子,
// 池子里面的对象啥时候真正释放是由 go_runtime进行回收,是不受外部控制的
// defer 还回去
defer pool.Put(u)
// 紧接着重置 u 这个对象
u.Reset("Tom", "my_email@qq.com")
// 下边就是使用 u 来完成你的业务逻辑
fmt.Printf("%+v", u)
}
type user struct {
Name string
Email string
}
// 一般来说,复用对象都要求我们取出来之后,
// 重置里面的字段
func (u *user) Reset(name string, email string) {
u.Email = email
u.Name = name
}