sync.Mutex与sync.RWMutex
保证多个并发线程对共享资源的访问是串行的,否则很容易出现争用和冲突的情况,这时需要使用互斥量来保证在同一时刻只有一个goroutine访问共享资源,其中sync.Mutex与sync.RWMutex类型就是互斥量,也称互斥锁,
当有goroutine进入临界区时,我们对他进行锁定,当他离开时我们进行解锁操作,锁定操作可以用Lock方法实现,解锁操作可以用Unlock方法
var mu sync.Mutex //定义互斥锁
mu.Lock()
... //临界区
mu.Unlock()
1:不要重复锁定互斥锁
2:不要忘记解锁,适当使用defer语句
3:不要在多个函数传递互斥锁 (因为值传递时会产生副本,副本也是独立的互斥锁)
4:不要对未锁定的互斥锁解锁
sync.RWMutex就是读写互斥锁,比起sync.Mutex更加细致,读操作和写操作分别保护,多个写操作不能同时进行,读操作和写操作也不可以同时进行,但是多个读操作可以同时进行。
1:写锁已经锁定时,试图锁定读锁或者锁定写锁都会阻塞当前goroutine
2:读锁已经锁定时,试图锁定写锁时会阻塞goroutine
读锁写锁的锁定解锁分别用RLock(),RUnlock(),Lock(),Unlock().
sync.Cond
条件变量是基于互斥锁的,有互斥锁才能发挥作用,但是条件变量的主要作用并不是保护共享资源的,是用来通知给别的goroutine共享资源的状态。
条件变量的方法:wait(等待通知),signal(单发通知),broadcast(广发通知)
sync.Cond类型需要初始化才可以使用,传入的值需要是sync.Locker类型的参数,sync.Locker是一个接口,实现的方法为Lock(),UnLock(),sync.Mutex与sync.RWMutex都拥有这两个方法,但是是指针方法,所以传入的参数也要是指针类型
代码详见:Golang_Puzzlers/demo61.go at master · hyper0x/Golang_Puzzlers (github.com)
package main
import (
"log"
"sync"
"time"
)
func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是满的。
var mailbox uint8
// lock 代表信箱上的锁。
var lock sync.RWMutex
// sendCond 代表专用于发信的条件变量。
sendCond := sync.NewCond(&lock)
// recvCond 代表专用于收信的条件变量。
recvCond := sync.NewCond(lock.RLocker())
// sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
max := 5
go func(max int) { // 用于发信。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500)
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
log.Printf("sender [%d]: the mailbox is empty.", i)
mailbox = 1
log.Printf("sender [%d]: the letter has been sent.", i)
lock.Unlock()
recvCond.Signal()
}
}(max)
go func(max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= max; j++ {
time.Sleep(time.Millisecond * 500)
lock.RLock()
for mailbox == 0 {
recvCond.Wait()
}
log.Printf("receiver [%d]: the mailbox is full.", j)
mailbox = 0
log.Printf("receiver [%d]: the letter has been received.", j)
lock.RUnlock()
sendCond.Signal()
}
}(max)
<-sign
<-sign
}
wait方法具体操作:
1:把当前goroutine加入到条件变量等待队列
2:把当前条件变量基于的锁解锁
3:收到通知时决定是否唤醒当前等待的goroutine,
4:唤醒goroutine之后重新锁定互斥锁
Signal与Broadcast方法不同之处在于前者只会唤醒一个等待的goroutine,后者会唤醒所有等待的goroutine,Signal唤醒的goroutine一般都是最早等待的那个。这两个方法不需要互斥锁保护的时候进行,相反解锁之后进行会对程序更有利
原子操作
go语言在开启多个goroutine时,同一时刻在底层支持的数量可能不会超过核心线程数,所以调度器会频繁的运行/停止这些goroutine,所以还是会影响运行的效率,因为会被打断。真正实现原子性执行的只有原子操作,原子操作进行时不会被打断,这代表执行速度更快,不过正因为不会被打断,所以他需要更简单更快速,因此支持的只有整数和二进制位的原子操作。
在sync/atomic包中的函数提供了修改、读取、写入、交换、比较并交换
比较并交换操作,也叫CAS操作,与互斥锁不同的是,互斥锁在假设goroutine频繁更改共享资源而使用,而CAS操作恰恰相反,它往往是假设共享资源更改不频繁而使用,也被称为乐观锁。
代码举例:
var value int32
func AddValue(delta int32) {
for {
v:= value
if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
break
}
}
}
由于原子操作类型过于局限,互斥锁往往更常用一些
sync/atomic.Value
它只有两个方法,Store和Load用来提供原子性的读写值操作,可以说是原子性的一个容器
注意:
1:我们不能用Store方法传入nil,会引发panic,(如果有一个接口类型的变量,它的动态值是nil,但动态类型却不是nil,那么它的值就不等于nil。这样一个变量的值是可以被存入原子值的。)
2:我们向这里存储的第一个值的类型决定了只能存储什么类型的值
还有,我们尽量不要传入引用类型的值,这样是不安全的,因为我们可以绕过原子值去修改原子值内部的引用值,比如:
var box6 atomic.Value
v6 := []int{1, 2, 3}
box6.Store(v6)
v6[1] = 4 // 注意,此处的操作不是并发安全的!
解决方法,我们可以把引用类型的值副本传入原子值
store := func(v []int) {
replica := make([]int, len(v))
copy(replica, v)
box6.Store(replica)
}
store(v6)
v6[2] = 5 // 此处的操作是安全的。
sync.WaitGroup和sync.Once
sync包中的WautGroup类型更适合实现一对多的goroutine协作流程,他有三个指针方法:Add、Done、Wait
Add:一般时候用来记录需要等待的goroutine的数量,进行计数器加操作
Done:与Add相对进行计数器的减操作
wait:阻塞当前goroutine,直到计数器为0
代码对比:(Golang_Puzzlers/demo65.go at master · hyper0x/Golang_Puzzlers (github.com))
func coordinateWithChan() {
sign := make(chan struct{}, 2)
num := int32(0)
fmt.Printf("The number: %d [with chan struct{}]\n", num)
max := int32(10)
go addNum(&num, 1, max, func() {
sign <- struct{}{}
})
go addNum(&num, 2, max, func() {
sign <- struct{}{}
})
<-sign
<-sign
}
func coordinateWithWaitGroup() {
var wg sync.WaitGroup
wg.Add(2)
num := int32(0)
fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
max := int32(10)
go addNum(&num, 3, max, wg.Done)
go addNum(&num, 4, max, wg.Done)
wg.Wait()
}
// addNum 用于原子地增加numP所指的变量的值。
func addNum(numP *int32, id, max int32, deferFunc func()) {
defer func() {
deferFunc()
}()
for i := 0; ; i++ {
currNum := atomic.LoadInt32(numP)
if currNum >= max {
break
}
newNum := currNum + 2
time.Sleep(time.Millisecond * 200)
if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i)
} else {
fmt.Printf("The CAS operation failed. [%d-%d]\n", id, i)
}
}
}
注意当WaitGroup中的计数器值小于0时会·引发panic,也不要用不同的goroutine去分别进行Add/Done和Wait,会有几率引发panic
go语言sync代码包中waitgroup_test.go部分代码,展示了异常情况的发生条件
func TestWaitGroupMisuse(t *testing.T) {
defer func() {
err := recover()
if err != "sync: negative WaitGroup counter" {
t.Fatalf("Unexpected panic: %#v", err)
}
}()
wg := &WaitGroup{}
wg.Add(1)
wg.Done()
wg.Done()
t.Fatal("Should panic")
}
sync.Once类型也是开箱即用的,其中的Do方法只接受一个参数,类型必须是func(),是一个无参数声明和结果声明的函数,这个方法只会执行首次被调用时传入的函数,之后不会执行任何函数参数,Once内部包含的done字段用来判断Do方法是否调用完成,所以值只是0或1
1:Do方法会在参数函数执行完毕时把done字段置为1
2:done值的修改和读取都是原子操作,所以就算参数函数引发panic,程序也无法在用这个Once值去执行他了
sync.Pool
go语言中的临时对象池,被用来存储临时对象,可以针对数据的缓存使用,这个类型只有两个方法,Get和Put
Get:用于获取当前池中的临时对象
Put:用于存放临时对象
如果Get方法使用时,池中没有对象那么这个方法会用sync.Pool类型的New字段创建对象并返回
New的类型是func() interface{}
New字段需要初始化对象池时就给定一个值,fmt包中就使用到了。
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
有关临时对象池的清理,引用郝林老师的一段话:
sync包在被初始化的时候,会向 Go 语言运行时系统注册一个函数,这个函数的功能就是清除所有已创建的临时对象池中的值。我们可以把它称为池清理函数。
一旦池清理函数被注册到了 Go 语言运行时系统,后者在每次即将执行垃圾回收时就都会执行前者。
另外,在sync包中还有一个包级私有的全局变量。这个变量代表了当前的程序中使用的所有临时对象池的汇总,它是元素类型为*sync.Pool的切片。我们可以称之为池汇总列表。
通常,在一个临时对象池的Put方法或Get方法第一次被调用的时候,这个池就会被添加到池汇总列表中。正因为如此,池清理函数总是能访问到所有正在被真正使用的临时对象池。
更具体地说,池清理函数会遍历池汇总列表。对于其中的每一个临时对象池,它都会先将池中所有的私有临时对象和共享临时对象列表都置为nil,然后再把这个池中的所有本地池列表都销毁掉。
最后,池清理函数会把池汇总列表重置为空的切片。如此一来,这些池中存储的临时对象就全部被清除干净了。
sync.Map
用不同的goroutine操作原生字典是不安全的,所以诞生了并发安全字典
并发安全字典同样对键值类型有要求,不能是函数类型,字典类型,切片类型。我们可以用类型断言表达式或者反射来保证类型正确
引用郝林极客时间《Go语言核心三十六讲》:
代码:Golang_Puzzlers/demo72.go at master · hyper0x/Golang_Puzzlers (github.com)
type IntStrMap struct {
m sync.Map
}
func (iMap *IntStrMap) Delete(key int) {
iMap.m.Delete(key)
}
func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
v, ok := iMap.m.Load(key)
if v != nil {
value = v.(string)
}
return
}
func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
a, loaded := iMap.m.LoadOrStore(key, value)
actual = a.(string)
return
}
func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
f1 := func(key, value interface{}) bool {
return f(key.(int), value.(string))
}
iMap.m.Range(f1)
}
func (iMap *IntStrMap) Store(key int, value string) {
iMap.m.Store(key, value)
}
编写了一个名为IntStrMap的结构体类型,它代表了键类型为int、值类型为string的并发安全字典。在这个结构体类型中,只有一个sync.Map类型的字段m。并且,这个类型拥有的所有方法,都与sync.Map类型的方法非常类似。
两者对应的方法名称完全一致,方法签名也非常相似,只不过,与键和值相关的那些参数和结果的类型不同而已。在IntStrMap类型的方法签名中,明确了键的类型为int,且值的类型为string。
显然,这些方法在接受键和值的时候,就不用再做类型检查了。另外,这些方法在从m中取出键和值的时候,完全不用担心它们的类型会不正确,因为它的正确性在当初存入的时候,就已经由 Go 语言编译器保证了。
稍微总结一下。第一种方案适用于我们可以完全确定键和值的具体类型的情况。在这种情况下,我们可以利用 Go 语言编译器去做类型检查,并用类型断言表达式作为辅助,就像IntStrMap那样。
第二种方案:
type ConcurrentMap struct {
m sync.Map
keyType reflect.Type
valueType reflect.Type
}
func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
if keyType == nil {
return nil, errors.New("nil key type")
}
if !keyType.Comparable() {
return nil, fmt.Errorf("incomparable key type: %s", keyType)
}
if valueType == nil {
return nil, errors.New("nil value type")
}
cMap := &ConcurrentMap{
keyType: keyType,
valueType: valueType,
}
return cMap, nil
}
func (cMap *ConcurrentMap) Delete(key interface{}) {
if reflect.TypeOf(key) != cMap.keyType {
return
}
cMap.m.Delete(key)
}
func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
if reflect.TypeOf(key) != cMap.keyType {
return
}
return cMap.m.Load(key)
}
func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
if reflect.TypeOf(key) != cMap.keyType {
panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
}
if reflect.TypeOf(value) != cMap.valueType {
panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
}
actual, loaded = cMap.m.LoadOrStore(key, value)
return
}
func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {
cMap.m.Range(f)
}
func (cMap *ConcurrentMap) Store(key, value interface{}) {
if reflect.TypeOf(key) != cMap.keyType {
panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
}
if reflect.TypeOf(value) != cMap.valueType {
panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
}
cMap.m.Store(key, value)
}
相比于第一种方案,第二种方案的键和值的类型更改更灵活,主要应用反射的知识,但是这样可能也会影响程序的性能,另外为了提高并发字典的性能,其中包含了read,和dirty字段,是两个原生字典,这里就不过多介绍了