文章目录
sync主要是干什么?
sync包是一个非常重要的工具包。它提供了一组用于同步操作的原语,可以帮助你编写线程安全的并发代码。在Go语言中,常常使用goroutines(轻量级线程)来实现并发,而sync包提供的工具可以确保这些goroutines之间的安全交互。
sync.Once
作用:
确保某个动作只执行一次,例如单例模式,初始化资源
简单使用:
type MyBiz struct {
once sync.Once
}
func (m *MyBiz) Init() {
// 这里 m *MyBiz 得用指针,不然其他地方再用,就变成了复制了,就不是只执行一次, 或者上面结构体定义时候使用指针定义变量
// 这个func只执行一次
m.once.Do(func() {
})
}
单例模式代码
// Singleton *******************单例模式*******************
// 就是下面这个结构体在系统中有且只有一个实例
type Singleton struct {
data string
}
var instance *Singleton // 单例对象实例
var once sync.Once
// GetInstance 获取单例实例的函数
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{
data: "Hello, I am a singleton!",
}
})
return instance
}
// *******************单例模式*******************
测试用例:
func TestGetInstance(t *testing.T) {
// 创建多个 Goroutine 获取单例实例
for i := 0; i < 5; i++ {
go func() {
s := GetInstance()
fmt.Println(s.data)
fmt.Printf("s 的内存地址:%p\n", s)
}()
}
time.Sleep(3 * time.Second)
}
锁
优先使用读写锁
互斥锁: sync.Mutex 读写都是独占
加锁之后协程独占这个锁,在大量并发的情况下,会造成锁等待,对性能的影响比较大。
不管锁是被reader还是writer持有,Lock方法会一直阻塞,Unlock用来释放锁的方法。
正常模式:效率高,新来的goroutine直接先抢锁,无需先排队,等待超过1ms,则切换到饥饿模式 。 为什么正常模式效率高:减少调度开销,新来的不用进入队列;可以充分利用缓存。
饥饿模式:更公平,等待超过1ms,把锁给排队的第一个goroutine,新来的goroutine也要先排队,先来后到。
自旋锁:当线程没有获得锁,循环等待锁的释放。
适用于 - 并发低但程序执行时间短的场景。
优点 - 避免线程上下文切换,执行时间短。
缺点 - cpu占用高。
阻塞锁:当线程没有获得锁,阻塞起来,把cpu给其他线程,获得锁之后唤醒阻塞。
适用于 - 高并发场景。
优点 - cpu占用低。
缺点 - 有线程上下文切换的开销。
互斥锁mutex实现了自旋和阻塞两种场景,不满足自选时候,会进入阻塞。
读写锁: sync.RWMutex 写独占,读共享
加了读锁,其他协程同样可以加读锁读取数据,加了写锁,其他协程无法读写。
RWMutex 结构:
type RWMutex struct {
w Mutex // 互斥锁,写 协程获得该锁后,其他协程处于等待
writerSem uint32 // writer 等待 读完成排队的信号量
readerSem uint32 // read 等待 write 完成排队的信号量
readerCount int32 // 读锁的计数器
readerWait int32 // 等待读锁释放的数量
}
所以:明确区分reader和writer的协程场景,且是大量的并发读、少量的并发写,有强烈的性能需要,我们就可以考虑使用读写锁RWMutex替换Mutex。
读写锁特点:
1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
2. 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
3. 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。
简单使用
type resource struct {
data interface{}
lock sync.Mutex // 把锁和数据封装在一起
}
func (r *resource) DoSomeThing() {
r.lock.Lock() // 上锁
defer r.lock.Unlock() // 解锁
// 在这里干活
}
同个函数中读写同时存在时,double_check的写法
// LoadOrStore 把key和 newVal写入
// 如果key值已经存在,loaded返回true, val返回原来key对应的value值
// 如果key值不存在, loaded返回false, val返回新写入的newVal
func (s *SafeMap[k, v]) LoadOrStore(key k, newVal v) (val v, loaded bool) {
s.mutex.RLock()
res, ok := s.data[key]
s.mutex.RUnlock() // 不是defer的锁要放在return之前, 防止锁未释放
if ok {
return res, true
}
// 到这里说明key值不存在,要写入,就加写锁
s.mutex.Lock()
defer s.mutex.Unlock()
// double-check,
// 再检查一遍, 因为如果第一个协程到这里卡住了,没有修改数据,第二个到这里协程就会觉得没有这个key,
// 从而用自己的value把第一个协程中正确的value覆盖
res, ok = s.data[key]
if ok {
return res, true
}
s.data[key] = newVal
return newVal, false
}
sync.Pool
介绍:
- 本质上是一个可以存放和取回任何类型的临时对象的容器。
- 它在任何时候都可以安全地将对象放入池中或者获取对象,而且不需要进行任何形式的同步操作。它在保存和获取对象时都是并发安全的。
- 解决高并发下的内存泄漏问题。
- 避免频繁的内存分配和垃圾回收,提升性能。
- 当我们需要创建大量的临时对象,并且想要在使用后重用它们的时候,这个数据结构就非常有用了。
上简单代码:
import (
"fmt"
"sync"
"testing"
)
type Person struct {
Name string
}
func TestPool(t *testing.T) {
// 创建一个 sync.Pool 资源池对象,用于存放和获取 Person 类型实例的容器
// New 函数返回一个内存分配器,当 sync.Pool 需要分配新内存时调用它
pool := &sync.Pool{
New: func() interface{} {
// 这里只会打印一次,创建完成了,外面再把对象放进来,就会复用第一个创建的对象,所以就不会再进行 New 操作了
fmt.Println("创建对象")
// 一般不要返回nil, 因为外面进行类型断言时候如果是nil会出问题
return &Person{}
},
}
// 获取实例
f := pool.Get().(*Person)
fmt.Println("获取到对象")
// 设置 f 对象的 Name 属性
f.Name = "i am jack"
fmt.Println("设置对象 f.Name to === ", f.Name)
// 将 f 实例放回 Pool 中, 就放回原来的那个对象上,不会新建
pool.Put(f)
fmt.Println("把对象再放回资源池中")
// 再次获取实例,因为我们已经放入了一个 foo 实例,所以这次取出的实例应该是刚才放入的那个
f2 := pool.Get().(*Person)
if f == f2 {
fmt.Println("再次获取的对象和之前是同一个")
}
}
sync.WaitGroup
介绍
- waitGroup主要用来同步和控制多个 goroutine 之间的工作.
- waitGroup常被用于等待一组Goroutine完成, 然后进行统计工作或者进入下一步。
sync.WaitGroup有三个方法:
- Add(int): 这会增加WaitGroup计数器。通常在启动新的Goroutine之前调用。
- Done(): 这会减少WaitGroup计数器。通常在Goroutine结束时调用。
- Wait(): 这会阻塞,直到WaitGroup计数器变为0。通常在主Goroutine中调用,等待所有其他Goroutine完成。
注意: Add() 和 Done() 一定要对应,加了1,在小任务结束后必须调用 Done()来减1,不然会有问题:
1.Add() 加多了导致一直wait,导致goroutine泄露。
2.Done()减多了直接panic。
上简单代码
func TestWaitGroup(t *testing.T) {
ws := sync.WaitGroup{}
var result int64 = 0 // 存储统计数
// 统计从 0 加到 10 的数
for i := 0; i <= 10; i++ {
ws.Add(1) // 启动goroutine之前,先加一
go worker(i, &result, &ws)
}
ws.Wait() // 等待waiGroup结束
t.Log("result:", result)
}
func worker(i int, result *int64, ws *sync.WaitGroup) {
defer ws.Done() // goroutine结束,计数器减一
atomic.AddInt64(result, int64(i))
}