@@ -1,58 +0,0 @@ 28.sync-lock锁 | 凤凰涅槃进阶之路

28.sync-lock锁

Abel sun2023年1月6日约 1256 字大约 4 分钟

28.sync-lock锁

有时候在Go代码中可能会存在多个goroutine同时操作一个公共资源(临界区),这种情况会发生竞态问题(数据竞态)。此处可以用火车上的公共卫生间被车厢里的人竞争。例子如下:

package main

import (
 "fmt"
 "sync"
)

var x = 0
var wg sync.WaitGroup

func add() {
 for i := 0; i < 5000; i++ {
  x = x + 1
 }
 defer wg.Done()
}
func main() {
 wg.Add(2)
 go add()
 go add()
 wg.Wait()
 fmt.Println(x)
}

当我们执行程序,打印最终的x,会发现得到的最终结果与预期的结果并不一样,这就是主函数中两个goroutine在同时访问公共资源x时会有一些竞争,从而导致最终结果不一致。此时可以通过引入锁的机制来解决此问题,就想火车上进入卫生间之后,先上锁,从而不会有别人再影响到。

1,互斥锁--sync.Mutex

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

如下:

package main

import (
 "fmt"
 "sync"
)

var x = 0
var wg sync.WaitGroup
var lock sync.Mutex

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

如此执行如上代码,则总是能得到10000的结果。

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁,当互斥锁释放后,等待的goroutine才可以获取进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

2,读写互斥锁--sync.RWMutex

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁更好的一种选择。读写锁在Go语言中使用sync包中的RWMuex类型。

读写锁分为两种,读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁就会继续获得读锁,如果是获得写锁就会等待,当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main

import (
 "fmt"
 "sync"
 "time"
)

var (
 x      = 0
 wg     sync.WaitGroup
 lock   sync.Mutex
 rwlock sync.RWMutex
)

func write() {
 defer wg.Done()
 // lock.Lock()
 rwlock.Lock()
 x = x + 1
 time.Sleep(time.Millisecond * 5)
 // lock.Unlock()
 rwlock.Unlock()
}
func read() {
 defer wg.Done()
 // lock.Lock()
 rwlock.RLock()
 fmt.Println(x)
 time.Sleep(time.Millisecond)
 // lock.Unlock()
 rwlock.RUnlock()
}
func main() {
 start := time.Now()
 for i := 0; i < 10; i++ {
  wg.Add(1)
  go write()
 }
 time.Sleep(time.Second)
 for i := 0; i < 1000; i++ {
  wg.Add(1)
  go read()
 }
 wg.Wait()
 fmt.Println(time.Now().Sub(start))
}

需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。

3,sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件,只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案---sync.Once。其函数如下:

func (o *Once) Do(f func()){}

备注:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

package main

import (
 "fmt"
 "sync"
)

var wg sync.WaitGroup
var once sync.Once

func f1(ch1 chan<- int) {
 defer wg.Done()
 for i := 0; i < 100; i++ {
  ch1 <- i
 }
 close(ch1)
}
func f2(ch1 <-chan int, ch2 chan<- int) {
 defer wg.Done()
 for {
  x, ok := <-ch1
  if !ok {
   break
  }
  ch2 <- x * x
 }
 // 因为某个操作只执行一次,因为下边主函数调用当前函数开启了两个goroutine运行,如果单独写个close,就会报错了
 // 此时就可以引入 sync.Once 的操作
 once.Do(func() { close(ch2) }) 
}
func main() {
 a := make(chan int, 100)
 b := make(chan int, 100)
 wg.Add(3)
 go f1(a)
 go f2(a, b)
 go f2(a, b)
 wg.Wait()
 for ret := range b {
  fmt.Println(ret)
 }
}

4,sync.Map

Go语言中内置的map并不是并发安全的,如下示例:

package main

import (
 "fmt"
 "strconv"
 "sync"
)

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()
}

运行之后,会报如下错误:

k=19,v=19
k=9,v=9
k=4,v=4
fatal error: k=15,v=15
concurrent map writes
fatal error: concurrent map writes
k=16,v=16

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报 fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map--sync.Map。其内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

package main

import (
 "fmt"
 "strconv"
 "sync"
)

var m = sync.Map{}

func main() {
 wg := sync.WaitGroup{}
 for i := 0; i < 50; i++ {
  wg.Add(1)
  go func(n int) {
   key := strconv.Itoa(n)
   m.Store(key, n)
   value, _ := m.Load(key)
   fmt.Printf("key=%v,v=%v\n", key, value)
   wg.Done()
  }(i)
 }
 wg.Wait()
}
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.9.1