Go语言中的并发安全和锁

如果没有锁

在我们的项目中,可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。
直接代码解释:

// 多个goroutine并发操作全局变量x
var x int64
var wg sync.WaitGroup

func add()  {
    for i :=0;i<1000;i++{
        x = x + 1
    }
    wg.Done()
}

func main()  {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

开启两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

互斥锁能够保证同时只有一个goroutine可以访问共享资源,是一种常用的控制共享资源访问的方法。Go语言中使用sync包的Mutex类型来实现互斥锁。
上述代码优化:

// 多个goroutine并发操作全局变量x
var x int64
var wg sync.WaitGroup
var lock sync.Mutex // 互斥锁

func add()  {
    for i :=0;i<1000;i++{
        lock.Lock()     // 加锁
        x = x + 1
        lock.Unlock()   // 解锁
    }
    wg.Done()
}
func main()  {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
但是,这种方式还是有问题的,读写都会等待,大大降低了程序效率。

读写互斥锁

在大多数场景下,是读多写少的,,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的。这种情况就可以使用读写互斥锁,Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
我们通过互斥锁和读写锁分别来检验下代码执行效率:
互斥锁代码示例:

// 读写互斥锁
var (
    x int64
    lock sync.Mutex
    wg  sync.WaitGroup
)
func read()  {
    lock.Lock()     // 读加锁
    time.Sleep(time.Millisecond)        // 读操作耗时1ms
    lock.Unlock()   //解锁
    wg.Done()
}

func write()  {
    lock.Lock()     // 写加锁
    x += 1
    time.Sleep(time.Millisecond *2) // 写操作耗时2ms
    lock.Unlock()
    wg.Done()
}

func main()  {
    start := time.Now()
    for i:=0;i<100;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))
}

互斥锁耗时大概1.6s,换成读写锁如下:

// 读写互斥锁
var (
    x int64
    lock sync.RWMutex
    wg  sync.WaitGroup
)
func read()  {
    lock.RLock()        // 读加锁
    time.Sleep(time.Millisecond)        // 读操作耗时1ms
    lock.RUnlock()  //解锁
    wg.Done()
}

func write()  {
    lock.RLock()        // 写加锁
    x += 1
    time.Sleep(time.Millisecond *2) // 写操作耗时2ms
    lock.RUnlock()
    wg.Done()
}

func main()  {
    start := time.Now()
    for i:=0;i<100;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))
}

读写锁耗时在4ms左右。
读写锁非常适合读多写少的场景!

sync.WaitGroup

Go语言中可以使用sync.WaitGroup来实现并发任务的同步。
sync.WaitGroup有以下几个方法:

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器 + delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

sync.Map

Go语言中内置的map不是并发安全的,当并发操作map时,就会出现fatal error: concurrent map writes的错误。

var (
    wg sync.WaitGroup
    m  = make(map[int]int)
)

func get(key int) int {
    return m[key]
}

func set(key int, value int) {
    m[key] = value
}

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            set(i, i+10)
            fmt.Println(i, get(i))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map,sync.Map。
代码优化如下:

// sync.map 并发安全的map
var (
    wg sync.WaitGroup
    m  = sync.Map{}
)

func get(key int) interface{} {
    value, _ := m.Load(key)
    return value
}

func set(key int, value int) {
    m.Store(key, value+10)
}

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            set(i, i+10)
            fmt.Println(i, get(i))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

精简一下:

var (
    wg sync.WaitGroup
    m  = sync.Map{}
)

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            m.Store(i, i+10)
            value, _ := m.Load(i)
            fmt.Println(i, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}
posted @ 2021-06-24 10:50  我是一条最咸的咸鱼  阅读(544)  评论(0编辑  收藏  举报
返回顶部