go语言sync包


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

介绍:
  1. 本质上是一个可以存放和取回任何类型的临时对象的容器。
  2. 它在任何时候都可以安全地将对象放入池中或者获取对象,而且不需要进行任何形式的同步操作。它在保存和获取对象时都是并发安全的。
  3. 解决高并发下的内存泄漏问题。
  4. 避免频繁的内存分配和垃圾回收,提升性能。
  5. 当我们需要创建大量的临时对象,并且想要在使用后重用它们的时候,这个数据结构就非常有用了。
上简单代码:
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

介绍
  1. waitGroup主要用来同步和控制多个 goroutine 之间的工作.
  2. 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))
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值