1. sync.Once介绍
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
1.1. 函数格式
sync.Once只有一个Do方法,其格式如下:
func (o *Once) Do(f func()) {}
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
1.2. 示例及解释
这里通过定义变量var once sync.Once
让其在多个协程中执行Do函数,最终能够保证协程安全,仅被执行一次. 具体实现原理可以参考文章Once原理.
once.Do 中的函数只会执行一次,并保证 once.Do 返回时,传入Do的函数已经执行完成。(多个 goroutine 同时执行 once.Do 的时候,可以保证抢占到 once.Do 执行权的 goroutine 执行完 once.Do 后,其他 goroutine 才能得到返回 )
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup //保证等待多个协程全部执行完再执行后续
var once sync.Once //保证仅被执行一次
onceBody := func() {
time.Sleep(3e9)
fmt.Println("Only once")
}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(int) {
defer wg.Done()
once.Do(onceBody)
fmt.Println(j)
}(i)
}
wg.Wait()
time.Sleep(3e9)
}
1.3. sync.Once封装结构
通常情况下,我们需要在once.Do()中的函数能够传递或返回某些变量,可以将其闭包使用;
//once.go
package once
import "sync"
type ReturnError struct {
once sync.Once
err error
}
func (owe *ReturnError) DoOnce(f func() error) error {
owe.once.Do(func() {
owe.err = f()
})
return owe.err
}
//示例测试函数,使用参考如下once_test.go
package once
import (
"errors"
"testing"
)
const (
testErrorString = "This is a test error"
)
func funcReturnErr() error {
err := errors.New(testErrorString)
return err
}
func TestMain(t *testing.T) {
owe := &ReturnError{}
//借用sync.once能保证 funcReturnErr只被执行一次
err := owe.DoOnce(funcReturnErr)
if err != nil {
t.Errorf("%v", err)
}
}
2. sync.Map
2.1. 示例及解释
Go语言中内置的map不是并发安全的。请看下面的示例:
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
上面的代码开启少量几个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 < 20; 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()
}
2.2. 函数格式
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {}
// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {}
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}){}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
//
// Range does not necessarily correspond to any consistent snapshot of the Map's
// contents: no key will be visited more than once, but if the value for any key
// is stored or deleted concurrently, Range may reflect any mapping for that key
// from any point during the Range call.
//
// Range may be O(N) with the number of elements in the map even if f returns
// false after a constant number of calls.
func (m *Map) Range(f func(key, value interface{}) bool) {}
3. 原子操作atomic
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。