package sync
import "sync"
sync
包提供了基本的同步基元,如互斥锁。除了Once
和WaitGroup
类型,大部分都是适用于低水平程序线程,高水平的同步使用channel
通信更好一些。
注意:本包的类型的值不应被拷贝。
sync.WaitGroup
type WaitGroup struct {
// 包含隐藏或非导出字段
}
WaitGroup
用于等待一组线程的结束。父线程调用Add
方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done
方法。同时,主线程里可以调用Wait
方法阻塞至所有线程结束。
WaitGroup
对象内部有一个计数器,最初从0
开始,它有三个方法:Add
, Done
, Wait
用来控制计数器的值:
func (wg *WaitGroup) Add(delta int)
:Add
方法向内部计数加上delta
,delta
可以是负数;如果内部计数器变为0
,Wait
方法阻塞等待的所有线程都会释放,如果计数器小于0
,方法panic
;func (wg *WaitGroup) Done()
:Done
方法使WaitGroup
计数器的值减1
,应在线程的最后执行;func (wg *WaitGroup) Wait()
:Wait
方法阻塞直到WaitGroup
计数器减为0
。
示例:
func f(wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(time.Second)
fmt.Printf("我是f\n")
}
func main() {
wg := sync.WaitGroup{}
wg.Add(1)
go f(&wg)
fmt.Println("我是main")
wg.Wait()
}
示例中WaitGroup
对象可以阻塞main
函数,直到所有goroutine
都运行完毕。
WaitGroup
对象是结构体类型,传参时需要使用地址,否则进程会死锁;也可以直接声明全局变量。
sync.Mutex:互斥锁
type Mutex struct {
// 包含隐藏或非导出字段
}
Mutex
是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex
类型的锁和线程无关,可以由不同的线程加锁和解锁。
Mutex
对象有两个方法:
func (m *Mutex) Lock()
:Lock
方法锁住m
,如果m
已经加锁,则阻塞直到m
解锁;func (m *Mutex) Unlock()
:Unlock
方法解锁m
,如果m
未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine
可以访问共享资源。
先举一个不加互斥锁的例子:
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
运行后会发现输出不是10000,而且无法预测。这是因为两个goroutine
可能同时取到x
,最后写入时相当于只进行了一次操作,导致结果出错。
对共享资源有修改操作时,可以使用互斥锁。在上例中加入互斥锁:
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 在访问共享资源前上锁
x = x + 1 // 上锁后其它goroutine无法访问x
lock.Unlock() // 访问完就解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
使用互斥锁后输出正确结果10000。
Mutex
对象是结构体类型,如果不使用全局变量,那么传参时需要使用地址。
sync.RWMutex:读写互斥锁
type RWMutex struct {
// 包含隐藏或非导出字段
}
RWMutex
是读写互斥锁。该锁可以被同时多个读取者持有或唯一个写入者持有。RWMutex
可以创建为其他结构体的字段;零值为解锁状态。RWMutex
类型的锁也和线程无关,可以由不同的线程加读取锁/写入锁和解读取锁/写入锁。
当一个goroutine
获取读取锁之后,其他的goroutine
如果是获取读取锁会继续获得锁,如果是获取写入锁就会等待;当一个goroutine
获取写入锁之后,其他的goroutine
无论是获取读取锁还是写入锁都会等待。
RWMutex
对象有 5 个方法:
func (rw *RWMutex) Lock()
:Lock
方法将rw
锁定为写入状态,禁止其他线程读取或者写入;func (rw *RWMutex) Unlock()
:Unlock
方法解除rw
的写入锁状态,如果rw
未加写入锁会导致运行时错误;func (rw *RWMutex) RLock()
:RLock
方法将rw
锁定为读取状态,禁止其他线程写入,但不禁止读取;func (rw *RWMutex) RUnlock()
:Runlock
方法解除rw
的读取锁状态,如果rw
未加读取锁会导致运行时错误;func (rw *RWMutex) RLocker() Locker
:Rlocker
方法返回一个互斥锁,通过调用rw.Rlock
和rw.Runlock
实现了Locker
接口(该接口包含Lock
和Unlock
两个方法)。
读写互斥锁的使用方式和互斥锁相同。对于读多写少的场景,如果使用互斥锁,那么共享资源始终只能被一个goroutine
访问,而读写互斥锁可以让多个只读取共享资源的goroutine
同时访问共享资源,这在读多写少场景中能大大节省时间。
sync.Once:保证函数只执行一次
type Once struct {
done uint32 // 表示函数是否已执行
m Mutex // 执行函数时需要上锁,保证只执行一次
}
Once
是只执行一次动作的对象。只有一个方法Do
:
func (o *Once) Do(f func())
Do
方法当且仅当第一次被调用时才执行函数f
。当Once
对象已经调用Do
方法后,即使更换f
函数也不会再执行,所以不同的f
函数需要声明不同的Once
对象。
因为f
是没有参数的,如果要执行的f
需要传递参数就需要搭配闭包来使用,比如:
config.once.Do(func() { config.init(filename) })
因为只有f
返回后Do
方法才会返回,所以f
若引起了Do
的调用,会导致死锁。
示例:
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
输出:
Only once
sync.Once
内部包含的互斥锁能保证f
的操作是并发安全的。
sync.Map:并发安全的映射
type Map struct {
// 包含隐藏或非导出字段
}
Map
类似于Go map[interface{}]interface{}
,但是可以安全地被多个goroutine
并发使用,而无需额外的锁定或协调。 加载,存储和删除以分摊的常数时间运行。
Map
类型是专用的。 大部分代码应改用带有单独锁定或协调功能的普通map,以提高类型安全性,并使其更易于维护其他不变量以及映射内容。
Map
类型针对两种常见用例进行了优化:(1)一个键值对仅写入一次但读取多次,例如在仅增长的cache中;(2)多个goroutine进行读取,写入和覆盖不相交的键值对。在这两种情况下,与搭配单独的Mutex或RWMutex的Go map相比,使用Map可以显著减少锁竞争(Lock Contention)。
Map
对象无需初始化,声明后即可直接使用。
Map
对象目前有6个方法:
func (m *Map) Delete(key interface{})
:Delete
方法删除key
对应的值;func (m *Map) Load(key interface{}) (value interface{}, ok bool)
:Load
方法返回key
对应的值,如果找不到则返回nil
,ok
表示是否找到值;func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
:LoadAndDelete
方法1.15版本才有,其删除key
对应的值并返回该值,loaded
表示key
是否存在;func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
:LoadOrStore
方法,如果key
存在则返回对应的值,如果不存在,则存储传入的key
和value
,返回传入的value
,loaded
为true
表示载入(load),为false
表示存储(store);func (m *Map) Range(f func(key, value interface{}) bool)
:Range
方法遍历Map
对象,函数f
接收参数key
和value
,返回一个bool
值,返回true
表示直接进行下一循环(接收下一对key-value
),返回false
表示结束循环(停止遍历)。遍历过程是无序的,与存储的顺序无关;func (m *Map) Store(key, value interface{})
:Store
方法存储键值对。
Range
方法示例:
var m1 sync.Map
m1.Store(1, 1)
m1.Store(2, 2)
m1.Store(3, 3)
f := func(k, v interface{}) bool {
fmt.Println(k, v)
if k == 3 {
return false
}
return true
}
m1.Range(f)
输出:
2 2
3 3
因为遍历是无序的,所以输出只是其中一次输出,可以看到遍历到3就结束了。
package atomic
import "sync/atomic"
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法,它在用户态就可以完成,因此性能比加锁操作更好。
atomic
包提供了底层的原子级内存操作,对于同步算法的实现很有用。
这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。
应通过通信来共享内存,而不通过共享内存实现通信。
atomic
包提供的函数如下:
读取系列
原子性的获取*addr
的值。
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
写入系列
原子性的将val
的值保存到*addr
。
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
修改系列(加减操作)
原子性的将delta
的值添加到*addr
并返回新值。
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
特别的,对于AddUint32
和AddUint64
,以AddUint64
为例,如果要让x
减去一个值c
,调用AddUint64(&x, ^uint64(c-1))
;让x
减1,调用AddUint64(&x, ^uint64(0))
。
交换系列
原子性的将new
值保存到*addr
并返回*addr
原来的值。
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
比较并交换系列
原子性的比较*addr
和old
,如果相同则将new
赋值给*addr
并返回真。
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)