介绍
在这个包下go语言给我提供了一下同步操作,在高并发的场景下非常适用,java程序员都知道,在jdk的标准库中也是给我们提供了GUC的并发包。在很多框架底层都是用到了它。那么go语言也有同样类似的包,那就是sync
该包下Locker就相当于java中的Lock其子类都有读锁与读写锁的实现。WaitGroup与CountDownLatch类似,syncMap与ConcurrentHashMap类似,Cond与Condition等
WaitGroup
WaitGroup可以等待一组goroutine的完成后,执行某个操作,大家在公司经常需要开会,每个会都会有几个主角,主角不到场一般是不会开始会议的。等待到场后,开始会议。
这样的场景在WaitGroup可以这样实现
package main
import (
"fmt"
"sync"
"time"
)
func main() {
group := sync.WaitGroup{}
// 今天会议有三个重要角色,需要人其才能开会
group.Add(3)
go func() {
time.Sleep(time.Second * 3)
fmt.Println("wendell已上线", time.Now().Format("2006-01-02:15:04:05"))
// 消耗 1
group.Done()
}()
go func() {
time.Sleep(time.Second * 5)
fmt.Println("juan已上线", time.Now().Format("2006-01-02:15:04:05"))
// 消耗 1
group.Done()
}()
go func() {
time.Sleep(time.Second * 10)
fmt.Println("老板已上线", time.Now().Format("2006-01-02:15:04:05"))
// 消耗 1
group.Done()
}()
// 等待state变为0了才会执行
group.Wait()
fmt.Println("会议开始...")
}
执行结果
wendell已上线 2022-11-12:14:12:15
juan已上线 2022-11-12:14:12:17
老板已上线 2022-11-12:14:12:22
会议开始...
Process finished with the exit code 0
Lock
加锁的场景就很常见了,比如多个协程需要操作同一个变量,如果不加锁可能就会造成错误的计算。放到电商项目就会造成商品库存的计算不正常造成损失。
比如这样一段代码,1000个goroutine同时操作count,不加锁的情况下看看结果是多少。如果大家测试不出来可以把1000改成10000
package main
import (
"fmt"
"sync"
)
var lock sync.Mutex
func main() {
count := 0
group := sync.WaitGroup{}
group.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
count++
group.Done()
}()
}
group.Wait()
fmt.Println(count)
}
运行结果
969
Process finished with the exit code 0
可以看到并不是我们想要的结果。这个时候就需要在count++时候加上一把锁
package main
import (
"fmt"
"sync"
)
var lock sync.Mutex
func main() {
count := 0
group := sync.WaitGroup{}
group.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
lock.Lock()
count++
lock.Unlock()
group.Done()
}()
}
group.Wait()
fmt.Println(count)
}
运行结果
1000
Process finished with the exit code 0
Map
很多场景下,我们都会用Map去保存一些key-value信息,用过java就知道,Map由于其底层的数据结构是并发不安全的,所以java出现了ConcurrentHashMap,实际上也是通过自旋锁来实现,go中也有类似的sync.Map
下面看看使用sync会是怎样
package main
import (
"fmt"
"sync"
)
func main() {
w := sync.WaitGroup{}
m := make(map[int]int)
for i := 0; i < 100; i++ {
w.Add(1)
go func(i int) {
defer w.Done()
m[i] = i
}(i)
}
w.Wait()
fmt.Println(m[50])
}
执行
报错 fatal error: concurrent map writes

提示的很明确,不能并发写。那么我们改用sync.Map
package main
import (
"fmt"
"sync"
)
func main() {
w := sync.WaitGroup{}
m := sync.Map{}
for i := 0; i < 100; i++ {
w.Add(1)
go func(i int) {
defer w.Done()
m.Store(i, i)
}(i)
}
w.Wait()
fmt.Println(m.Load(50))
}
执行
50 true
Process finished with the exit code 0
Once
从名字中就可以看出,不管你的并发多高,反正我就执行一次。比如关闭一些channel通道,初始化一个单列对象。配置文件的解析。
package main
import (
"fmt"
"sync"
)
func main() {
once := sync.Once{}
for i := 0; i < 10; i++ {
once.Do(func() {
fmt.Println("执行了...")
})
}
}
执行
执行了...
Process finished with the exit code 0
Cond
条件队列与java中的Condition非常类似,让goroutine进入等待状态,直到满足唤醒它的条件才可以继续往下执行,等待的goroutine会有多个,那么到底唤醒哪一个。于是就有了Signal唤醒一个和Broadcast唤醒所有的方法。
如下代码,有五个等待的goroutine,先唤醒第一个,等待三秒唤醒所有。
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"time"
)
func main() {
lock := &sync.Mutex{}
cond := sync.Cond{L: lock}
for i := 0; i < 5; i++ {
go func(i int) {
cond.L.Lock()
fmt.Println("进入等待")
cond.Wait()
fmt.Println("被唤醒", i, time.Now().Format("2006-01-02:15:04:05"))
cond.L.Unlock()
}(i)
}
time.Sleep(time.Second * 3)
// 先唤醒一个
cond.Signal()
time.Sleep(time.Second * 3)
// 唤醒所有
cond.Broadcast()
// 这里只是为了不让程序退出
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
<-ch
}
执行,观察被唤醒时间
进入等待
进入等待
进入等待
进入等待
进入等待
被唤醒 1 2022-11-12:14:40:44
被唤醒 3 2022-11-12:14:40:47
被唤醒 4 2022-11-12:14:40:47
被唤醒 0 2022-11-12:14:40:47
被唤醒 2 2022-11-12:14:40:47
有人可能好奇,上面循环语句中加了锁 cond.L.Lock() 为啥goroutine还能都打印出信息 “进入等待” 应该要等到goroutine被唤醒然后调用cond.L.Unlock()才能打印呀。
我们看看 cond.Wait() 的源码
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
发现这里把锁给释放了,然后再把自己进入等待状态。所以所有的goroutine都能够执行到 runtime_notifyListWait(&c.notify, t) 这个地方进入等待。
Pool
对象池,与锁一样,创建后就不能copy了,它在我们的程序中维护着一个临时对象。这个对象一段时间不用是会被垃圾回收的。所以像数据库的连接等信息就不适合用了。本质的目的就是提高性能,减少内存的使用
如果说一样的对象在一个应用程序中频繁被调用,那么就会不断New出新对象,占用内存,并发高的话还会出现故障。这个时候就可以使用Pool,重用对象,减少内存消耗,降低垃圾回收的压力
Pool本身原理还是稍显复杂,并且使用不当也会造成问题。所以这里先简单使用。后面在专门讲Pool
下面是一个简单例子
package main
import (
"fmt"
"strconv"
"strings"
"sync"
)
func main() {
pool := sync.Pool{}
pool.New = func() any {
return &strings.Builder{}
}
for i := 0; i < 10; i++ {
b := pool.Get().(*strings.Builder)
b.Reset()
b.WriteString("poll")
b.WriteString(":")
b.WriteString(strconv.Itoa(i))
b.WriteString(":")
b.WriteString(fmt.Sprintf("%p", b))
fmt.Println(b.String())
pool.Put(b)
}
}
执行
poll:0:0x14000132000
poll:1:0x14000132000
poll:2:0x14000132000
poll:3:0x14000132000
poll:4:0x14000132000
poll:5:0x14000132000
poll:6:0x14000132000
poll:7:0x14000132000
poll:8:0x14000132000
poll:9:0x14000132000
可以看到,对象地址都是一样的,起到了复用的作用。在gin框架中对于Context对象也是使用到了Pool
sync标准库就介绍到这吧,相信大家工作中也经常用到。赶紧学起来吧。
欢迎关注,学习不迷路!