Go sync并发工具包

简介

在Java中提供Sychronized关键字提供独占锁,Lock类提供读写锁。在sync包中实现的功能也是与锁相关,包中主要包含的有:

  • sync.Map:并发安全 map
  • sync.Mutex:锁
  • sync.RWMutex:读写锁
  • sync.Once:只执行一次
  • sync.WaitGroup: goroutine 之间同步
  • sync.Pool:复用缓存池

sync.Map

key 和 value 类型都是 Any。意味着你要搞各 种类型断言

image-20230203142912299

使用示例

package main

import (
	"fmt"
	"sync"
)

func main() {
	m := sync.Map{}
	m.Store("cat", "Tom")
	m.Store("mouse", "Jerry")

	// 这里重新读取出来的,就是
	val, ok := m.Load("cat")
	if ok {
		fmt.Println(len(val.(string)))
		fmt.Printf("%s \n", val)
	}
}

image-20230204181456737

sync.Mutex 和sync.RWMutex

使用案例

package main

import (
	"sync"
)

var mutex sync.Mutex
var rwMutex sync.RWMutex

func Mutex() {
	mutex.Lock()
	defer mutex.Unlock()
	// 你的代码
}

func RwMutex() {
	// 加读锁
	rwMutex.RLock()
	defer rwMutex.RUnlock()

	// 也可以加写锁
	rwMutex.Lock()
	defer rwMutex.Unlock()
}

// 不可重入例子
func Failed1() {
	mutex.Lock()
	defer mutex.Unlock()

	// 这一句会死锁
	// 但是如果你只有一个goroutine,那么这一个会导致程序崩溃
	mutex.Lock()
	defer mutex.Unlock()
}

// 不可升级
func Failed2() {
	rwMutex.RLock()
	defer rwMutex.RUnlock()

	// 这一句会死锁
	// 但是如果你只有一个goroutine,那么这一个会导致程序崩溃
	mutex.Lock()
	defer mutex.Unlock()
}

mutex家族注意事项

  • 尽量用 RWMutext
  • 尽量用 defer 来释放锁,防止panic没有释放锁
  • 不可重入:lock 之后,即便是同一个线程(goroutine),也无法再次加锁(写递归函数要小心)
  • 不可升级:加了读锁之后,如果试图加写锁,锁不升级

不可重入和不可升级,和很多语言的实现都是不同 的,因此要小心使用

sync.Once

package main

import (
	"fmt"
	"sync"
)

func main() {
	PrintOnce()
	PrintOnce()
	PrintOnce()
}

var once sync.Once

// 这个方法,不管调用几次,只会输出一次
func PrintOnce() {
	once.Do(func() {
		fmt.Println("只输出一次")
	})
}

image-20230204182436334

sync.WaitGroup

WaitGroup,它有3个函数:

  • Add():在被等待的协程启动前加1,代表要等待1个协程。
  • Done():被等待的协程执行Done,代表该协程已经完成任务,通知等待协程。
  • Wait(): 等待其他协程的协程,使用Wait进行等待。
type WaitGroup
func (wg *WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}

下怎么用WaitGroup实现上面的问题。

队长先创建一个WaitGroup对象wg,

每个队员都是1个协程, 队长让队员出发前,使用wg.Add(),

队员出发寻找钥匙,队长使用wg.Wait()等待(阻塞)所有队员完成,

某个队员完成时执行wg.Done(),等所有队员找到钥匙,

wg.Wait()则返回,完成了等待的过程,接下来就是开箱。

package main

import (
	"fmt"
	"sync"
)

func main() {
	// 钥匙
	key := 0
	// 资本家队长创建队伍
	wg := sync.WaitGroup{}
	// 这个队伍十个人
	wg.Add(10)
	for i := 1; i <= 10; i++ {
		go func(val int) {
			key = val
			// 打工人队员找到了钥匙
			wg.Done()
		}(i)
	}
	// 队长等待队伍集合,拿回钥匙,如果不等待,钥匙数量不定
	// 把这个注释掉你会发现,钥匙数量不知道会有多少个
	wg.Wait()
	fmt.Println(key)
}

sync.Pool

pool是什么

Golang在 1.3 版本的时候,在sync包中加入一个新特性:Pool。 简单的说:就是一个临时对象池

为什么需要sync.pool

保存和复用临时对象,减少内存分配,降低GC压力
对象越多GC越慢,因为Golang进行三色标记回收的时候,要标记的也越多,自然就慢了

sync.pool 特性

  1. 优先从自己的缓存里面返回数据
  2. 如果不够了,就创建一个新的,使用传入的 New 回调
  3. 在 GC 的时候,sync.Pool 会被清空

sync.pool 缺陷

  1. 没有超时机制
  2. 无法限制内存使用量
  3. GC 全部清掉

使用案例

sync.Pool 的一般使用步骤(以user为例)

  1. 定义 user 结构体
  2. 为 user 加上 Reset 方法。该方法用于重置对象, 接收器是指针
  3. 定义一个 sync.Pool
  4. 调用 Get 方法
  5. 使用defer 保证放回去 pool
  6. 重置 user
  7. 执行业务逻辑
package main

import (
	"fmt"
	"sync"
)

func main() {

	// 初始化一个pool
	pool := sync.Pool{
		// 默认的返回值设置,不写这个参数,默认是nil
		New: func() interface{} {
			return &user{}
		}}

	// Get 返回的是 interface{},所以需要类型断言
    // Get 方法会返回 Pool 已经存在的对象;如果没有就使用New方法创建.
	u := pool.Get().(*user)
    // 对象或资源不用时,调用 Put 方法把对象或资源放回池子,
    // 池子里面的对象啥时候真正释放是由 go_runtime进行回收,是不受外部控制的
	// defer 还回去
	defer pool.Put(u)

	// 紧接着重置 u 这个对象
	u.Reset("Tom", "my_email@qq.com")

	// 下边就是使用 u 来完成你的业务逻辑
	fmt.Printf("%+v", u)
}

type user struct {
	Name  string
	Email string
}

// 一般来说,复用对象都要求我们取出来之后,
// 重置里面的字段
func (u *user) Reset(name string, email string) {
	u.Email = email
	u.Name = name
}

image-20230204185807141

posted @ 2023-02-04 19:04  makalo  阅读(74)  评论(0编辑  收藏  举报