sync package
channel为什么是并发安全的呢?
channel内部使用了互斥锁来保证并发的安全
sync包让你对并发控制的得心应手
golang中不仅有channel这类比较易用且高级的同步机制
还有sync.Mutex
、sync.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
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: