Skip to main content

sync package

channel为什么是并发安全的呢?

channel内部使用了互斥锁来保证并发的安全

sync包让你对并发控制的得心应手

golang中不仅有channel这类比较易用且高级的同步机制

还有sync.Mutexsync.WaitGroup等比较原始的同步机制

资源竞争

在一个协程中,如果分配的内存没有被其他goroutine访问

只在该goroutine中被使用,那么不存在资源竞争问题

如果同一块内存被多个goroutine同时访问

就会产生不知道谁先访问也无法预料最后结果的情况

如下面的例子:

package main

import (
"fmt"
"time"
)

//共享的资源
var sum = 0

func main() {
//开启100个goroutine让sum+1
for i := 0; i < 100; i++ {
go add(1)
}
//防止提前退出
time.Sleep(2*time.Second)
fmt.Printf("sum:%d\n",sum)
}

func add(i int) {
sum +=i
}

运行几次看看结果:

可以看到sum结果有99 有100,导致这种结果的原因是:sum并不是并发安全的,因为它有多个协程会交叉的执行sum+i

要解决上面的问题 只需确保同时只有一个协程执行sum+i即可

小技巧

我们在使用go build/run/test 的时候可以加上--race 来检查go语言代码是否存在资源竞争

我们来测试上面的例子:

sync.Mutex

互斥锁指的是在同一时刻只有一个协程执行某段代码

其他协程都要等待该协程执行完毕后才能继续执行

package main

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

var (
sum = 0 //共享的资源
mutext sync.Mutex
)

func main() {
//开启100个goroutine让sum+1
for i := 0; i < 100; i++ {
go add(1)
}
//防止提前退出
time.Sleep(2*time.Second)
fmt.Printf("sum:%d\n",sum)
}

func add(i int) {
mutext.Lock()
sum +=i
mutext.Unlock()
}

上面的代码虽然能运行出sum=100,但是仍存在资源竞争

原因是第21行(fmt.Printf("sum:%d\n",sum))存在资源竞争

这个我们加个锁试试:

package main

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

var (
sum = 0 //共享的资源
mutext sync.Mutex
)

func main() {
//开启100个goroutine让sum+1
for i := 0; i < 100; i++ {
go add(1)
}
//防止提前退出
time.Sleep(2*time.Second)
mutext.Lock()
fmt.Printf("sum:%d\n",sum)
mutext.Unlock()
}

func add(i int) {
mutext.Lock()
sum +=i
mutext.Unlock()
}

可以看到已经是并发安全的了

也可以用defer来释放锁

临界区

指的是一个访问共享资源的程序片段,而这些共享资源又无法同时被多个协程访问的特性

sync.RWmutex

package main

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

var (
sum = 0 //共享的资源
mutext sync.Mutex
)

func main() {
//开启100个goroutine让sum+1
for i := 0; i < 100; i++ {
go add(1)
}
//开启10个协程 同时去读sum的值
for i := 0; i < 10; i++ {
go fmt.Println("和为:",readSum())
}
//防止提前退出
time.Sleep(2*time.Second)
}

func add(i int) {
mutext.Lock()
defer mutext.Unlock()
sum +=i
}

//增加了一个读取sum的函数 便于演示并发
func readSum() int{
b:= sum
return b
}

以上代码并不是并发安全的,因为一个协程在执行sum+1的时候, 另一个协程有可能正在读取sum的值, 这就会导致读取的值是过期的,因此结果是不可预期的

如果要解决以上资源竞争的问题,我们可以使用互斥锁,如下代码:

因为使用同一把锁(同一个实例), 所以当一个协程读取的时候,其他协程都会等待,反之亦然。

现在我们已经解决了资源竞争的问题,但同时又带来一个新的问题,就是

每次读写共享资源都要加锁,所以性能非常低

这该怎么解决呢?

我们来分析一下,并发读写可能出现下面3种情况:

  • 1,读的时候不能同时写:可能会出现不可预料的结果
  • 2,写的时候不能同时读:可能读到脏值
  • 3,读的时候可以同时读:因为数据没有改变,不管多少个协程去并发读都是安全的

所以我们可以通过读写锁(sync.RWmutex)来提升性能

读写锁的使用方法如下:

注意写的时候会等待,但是读的时候就不用互相等待了,所以大大提高了性能

sync.WaitGroup

前面我们在写并发的时候经常会用到time包让程序等待一下,main函数一退出,整个程序就结束了

上面的等待时间只是个模糊的数字,我们不能保证协程2s刚好执行完,有可能提前了(有性能丢失),也有可能压根都没执行完

怎样精准的控制呢?以保证协程执行完毕后,主程序刚好退出?这样既能保证协程全部执行完,主程序又能及时退出节省时间,提升性能

可能我们第一时间想到的是channel,的确channel确实可以解决这个问题,然而比较复杂,解决这类问题还有个简介的解决方案,那就是sync.WaitGroup

package main

import (
"fmt"
"sync"
)

var (
sum = 0 //共享的资源
mutext sync.RWMutex
)

func main() {
run()
fmt.Println(sum)
}

func run() {
var wg = sync.WaitGroup{}
//开启100个goroutine让sum+1
//因为要监控110个协程,所以设置计数器为110
wg.Add(110)
for i := 0; i < 100; i++ {
go func() {
//计数器值减1
defer wg.Done()
add(1)
}()
}

for i := 0; i < 10; i++ {
go func() {
//计数器值减1
defer wg.Done()
fmt.Println("和为:",sum)
}()
}
//防止提前退出
//一直等待 直到计数器恰好为0
wg.Wait()
}

func add(i int) {
mutext.Lock()
defer mutext.Unlock()
sum +=i
}

func readSum() int{
mutext.RLock()
defer mutext.RUnlock()
b:= sum
return b
}

这样你不论运行多少次,sum总和都是100, 我们多运行几次看看结果

注意:协程一定要调用 wg.Done(), 让计数器减1;最后要调用wg.Wait()一直等待 直到计数器恰好为0

应用场景

sync.WaitGroup 适合协调多个协程共同去做一件事情,用于最终完成的场景,关键点在于一定要等待其他所有人都执行完毕

eg:假设使用10个协程,每个协程下载文件的1/10大小,只有10个协程都下载好了,整个文件才算下载好了,这就是我们经常听说的多线程下载

你可以把go语言的协程理解成线程,在表现效果上是一致的,但是从技术层面的实现上却又是不一样的

线程由操作系统调度,协程go语言自己调度

sync.Once

现实中有这样的需求:让代码只执行一次,哪怕是在高并发的情况下

比如创建一个单例,初始化资源

package main

import (
"fmt"
"sync"
)

func main() {
doOnce()
}

func doOnce() {
var once sync.Once
onceBody := func() {
fmt.Println("only once")
}

//用于等待协程执行完毕
done := make(chan bool)

//启动10个协程执行 once.Do(onceBody)

for i := 0; i < 10; i++ {
go func() {
//把要执行的函数/方法作为参数传给once.Do()方法即可
once.Do(onceBody)
done <- true
}()
}

for i := 0; i < 10; i++ {
<-done
}
}

无论执行多少次,结果只会输出only once

Once使用场景

1,创建某个对象的单例 2,加载一次的资源

sync.Cond

sync.Cond不同于sync.WaitGroup,可以用于发号施令,关键点在于协程开始的时候都是等待的,要等待Cond去唤醒才能执行

Cond一声令下,所有协程都开始执行

下面以10人赛跑的案例来演示sync.Cond的用法,在这个案例中有一个裁判,裁判要等待这10个人准备就绪,然后一声发令声响,这10个人就可以开始跑了,如下代码所示:

package main

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

func main() {
race()
}

//10人赛跑,1个裁判发号施令
func race() {
//1.先用NewCond函数生成一个cond指针, 用于阻塞和唤醒协程
cond:=sync.NewCond(&sync.Mutex{})

var wg sync.WaitGroup
wg.Add(11)

for i := 0; i < 10; i++ {
go func(num int) {
defer wg.Done()
fmt.Println(num,"号已经就位")
//注意调用Wait函数的时候需要加锁
cond.L.Lock()
cond.Wait()//等待发令枪响
fmt.Println(num,"号开始跑...")
cond.L.Unlock()
}(i)
}

//等待所有的goroutine都进入wait的阻塞状态
time.Sleep(2*time.Second)

//裁判再去发号施令
go func() {
defer wg.Done()
fmt.Println("裁判员已就位,准备发令枪")
fmt.Println("比赛开始,大家准备跑")
cond.Broadcast()//发令枪响
}()

//防止函数提前退出
wg.Wait()
}

运行几次看看结果:

sync.Map

sync.Map和go语言原生的map差不多,只不过sync.Map是并发安全的

API 1.19.1

coding test

package main

import (
"fmt"
"sync"
)

func main() {
var m sync.Map

//store
fmt.Println("-------------test Store-------------")
m.Store("name", "Scott")
m.Store("sex", "M")
m.Store("location", "GD")

//LoadOrStore
fmt.Println("-------------test LoadOrStore-------------")
acture, flag := m.LoadOrStore("name", "Judy")
fmt.Println("LoadOrStore:", acture, flag)
acture2, flag2 := m.LoadOrStore("age", 18)
fmt.Println("LoadOrStore:", acture2, flag2)

//range
fmt.Println("t-------------est Range-------------")
m.Range(func(k, v interface{}) bool {
fmt.Printf("key:%s,value:%v\n", k, v)
return true
})

//delete
fmt.Println("-------------test Delete-------------")
m.Delete("location")
m.Delete("location1")

//range
fmt.Println("-------------test Range-------------")
m.Range(func(k, v interface{}) bool {
fmt.Printf("key:%s,value:%v\n", k, v)
return true
})

fmt.Println("-------------test Load-------------")
//load
value, exit := m.Load("age")
if exit {
fmt.Println(value)
} else {
fmt.Println("key => age not exist")
}
}

run: