Golang sync中map和once

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提供。

见文章Golang sync_atomic元子操作

发表评论

邮箱地址不会被公开。 必填项已用*标注