sync包
互斥锁
互斥锁是一种常见的控制资源访问的方法,它可以保证同时只有一个goroutine
可以访问临界资源。Go语言中可以使用sync
包的Mutex
类型来实现互斥锁。
例:
// 开启多个gorourtine执行add()操作,会导致两个goroutine读取到相同的x导致最终结果偏小
var x = 0
var wg sync.WaitGroup
func add() {
defer wg.Done()
for i := 0; i < 5000; i++{
x += 1
}
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
println(x)
}
改进:
var lock sync.Mutex
func add() {
defer wg.Done()
for i := 0; i < 5000; i++{
lock.Lock() // 加锁
x += 1
lock.Unlock() // 解锁
}
}
使用互斥锁能够保证同一时间有且仅有一个goroutine
进入临界区,其他的goroutine
则在等待锁。当互斥锁释放后,等待的goroutine
才可以获取锁进入临界区,多个goroutine
同时等待一个锁时,等待的策略是随机的。
读写互斥锁
互斥锁是完全互斥的,但是很多实际的场景下是读多写少的,当我们并发的读取一个资源不涉及资源修改时是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync
包的RWMutex
类型。
读写锁分为两种:读锁与写锁。当一个goroutine
获取读写锁之后,其他的goroutine
如果是获取读锁会继续加锁,如果是获取写锁就会继续等待;当一个goroutine
获取写锁后,其他的goroutine
无论是读锁还是写锁都会继续等待。
package main
import (
"fmt"
"sync"
"time"
)
var (
x int
wg sync.WaitGroup
//lock sync.Mutex
rwLock sync.RWMutex
)
func write() {
defer wg.Done()
// lock.Lock()
rwLock.Lock() // 加写锁
x += 1
time.Sleep(time.Millisecond * 10) // 模拟写操作
rwLock.Unlock() // 解写锁
// lock.Unlock() //
}
func read() {
defer wg.Done()
//lock.Lock() // 加互斥锁
rwLock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 模拟读操作
rwLock.RUnlock() // 解写锁
// lock.Unlock()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++{
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++{
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
读写锁适合读多写少的应用场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
sync.Once()
在编程的很多场景下需要确保某些操作在高并发场景下之执行一次,例如只加载一次配置文件,只关闭一次通道等。
Go语言中sync
提供了一个针对只执行一次场景的解决方案——sync.Once
。
sync.Once
只有一个Do
方法,签名:
func (o *Once) Do(f func()){}
如果要执行的函数f
需要传递参数需要搭配闭包来使用。
加载配置文件示例:
延迟一个开销很大的初始化操作到真正使用它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如再init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须的。
var icons map[string]image.Image
func loadIcons(){
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon被多个goroutine调用时不是并发安全
func Icon(name string) image.Image{
if icons == nil{
loadIcons()
}
return icons[name]
}
多个goroutine
并发调用Icon函数不是并发安全的,现代编译器和CPU可能会在保证每个goroutine
都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重拍为以下结果:
func loadIcons(){
icon = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在这种情况下就会出现即使判断力icons
不为nil也不意味着变量初始化完成了。因此,考虑添加互斥锁,保证初始化icons
的时候不会被其他的goroutine
操作,但这又引发性能问题。
使用sync.Once
改造的实例代码:
var icons map[string]image.Image
var loadIconOnce sync.Once
func loadIcons(){
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 并发安全
func Icon(name string) image.Image{
loadIconOnce.Do(loadIcons)
return icons[name]
}
sync.Once
内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计保证初始化操作时是并发安全且初始化操作时不会被执行多次。
sync.Map
Go语言中内置的map不是并发安全的,当仅开启少量几个goroutine
的时候可能没什么问题,当并发多了之后会导致fatal error: concurrent map writes
错误。
因此需要为map保证并发安全性,Go语言sync
提供了并发安全版map——sync.Map
,其不用像内置map一样使用make函数初始化就能直接使用。同时sync.Map
内置了诸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
var m sync.Map
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 30; 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()
}
atomic
互斥与原子操作
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// 原子操作
var x int64
var wg sync.WaitGroup
//var lock sync.Mutex
func add() {
defer wg.Done()
//lock.Lock()
atomic.AddInt64(&x, 1)
//x++
//lock.Unlock()
}
func main() {
wg.Add(100000)
for i := 0; i < 100000; i++{
go add()
}
wg.Wait()
fmt.Println(x)
}
atomic
提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正常使用。除了某些特殊的底层应用,使用通道或者sync
包的函数/类型实现同步比较好。