本节源码位置 github.com/golang-mini…

sync.Map 并发安全的Map

反例如下,两个Goroutine别离读写。

func unsafeMap(){
	var wg sync.WaitGroup
	m := make(map[int]int)
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			m[i] = i
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			fmt.Println(m[i])
		}
	}()
	wg.Wait()
}

履行报错:

0
fatal error: concurrent map read and map write
goroutine 7 [running]:
runtime.throw({0x10a76fa, 0x0})
......

运用并发安全的Map

func safeMap() {
	var wg sync.WaitGroup
	var m sync.Map
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			m.Store(i, i)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			fmt.Println(m.Load(i))
		}
	}()
	wg.Wait()
}
  • 不需求make就能运用
  • 还内置了StoreLoadLoadOrStoreDeleteRange等操作方法,自行体会。

sync.Once 只履行一次

很多场景下我们需求确保某些操作在高并发的场景下只履行一次,例如只加载一次配置文件、只封闭一次通道等。

init 函数是当地点的 package 初次被加载时履行,若迟迟未被运用,则既浪费了内存,又延长了程序加载时刻。

sync.Once 能够在代码的恣意位置初始化和调用,因而能够延迟到运用时再履行,并发场景下是线程安全的。

在大都情况下,sync.Once 被用于操控变量的初始化,这个变量的读写满足如下三个条件:

  • 当且仅当第一次拜访某个变量时,进行初始化(写);
  • 变量初始化过程中,一切读都被阻塞,直到初始化完结;
  • 变量仅初始化一次,初始化完结后驻留在内存里。
var loadOnce sync.Once
var x int
for i:=0;i<10;i++{
    loadOnce.Do(func() {
        x++
    })
}
fmt.Println(x)

输出1

sync.Cond 条件变量操控

sync.Cond 基于互斥锁/读写锁,它和互斥锁的差异是什么呢?

互斥锁 sync.Mutex 通常用来维护临界区和同享资源,条件变量 sync.Cond 用来和谐想要拜访同享资源的 goroutine

也便是在存在同享变量时,能够直接运用sync.Cond来和谐同享变量,比如最常见的同享行列,多消费多出产的形式。

我一开始也很疑问为什么不运用channelselect的形式来做出产者顾客模型(实际上也能够),这一节不是要点就不展开讨论了。

创立实例

func NewCond(l Locker) *Cond

NewCond 创立 Cond 实例时,需求相关一个锁。

广播唤醒一切

func (c *Cond) Broadcast()

Broadcast 唤醒一切等候条件变量 cgoroutine,无需锁维护。

唤醒一个协程

func (c *Cond) Signal()

Signal 只唤醒恣意 1 个等候条件变量 cgoroutine,无需锁维护。

等候

func (c *Cond) Wait()

每个 Cond 实例都会相关一个锁 L(互斥锁 *Mutex,或读写锁 *RWMutex),当修改条件或许调用 Wait 方法时,有必要加锁。

举个不恰当的例子,实现一个经典的出产者和顾客形式,但有先决条件:

  • 边出产边消费,能够多出产多消费。
  • 出产后告诉消费。
  • 行列为空时,暂停等候。
  • 支持封闭,封闭后等候消费完毕。
  • 封闭后依然能够出产,但无法消费了。
var (
	cnt          int
	shuttingDown = false
	cond         = sync.NewCond(&sync.Mutex{})
)
  • cnt 为行列,这儿直接用变量替代了,变量便是行列长度。
  • shuttingDown 消费封闭状态。
  • cond 现成的行列操控。

出产者

func Add(entry int) {
	cond.L.Lock()
	defer cond.L.Unlock()
	cnt += entry
	fmt.Println("出产咯,来消费吧")
	cond.Signal()
}

顾客

func Get() (int, bool) {
	cond.L.Lock()
	defer cond.L.Unlock()
	for cnt == 0 && !shuttingDown {
		fmt.Println("未封闭但空了,等候出产")
		cond.Wait()
	}
	if cnt == 0 {
		fmt.Println("封闭咯,也消费完咯")
		return 0, true
	}
	cnt--
	return 1, false
}

封闭程序

func Shutdown() {
	cond.L.Lock()
	defer cond.L.Unlock()
	shuttingDown = true
	fmt.Println("要封闭咯,我们快消费")
	cond.Broadcast()
}

主程序

var wg sync.WaitGroup
	wg.Add(2)
	time.Sleep(time.Second)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			go Add(1)
			if i%5 == 0 {
				time.Sleep(time.Second)
			}
		}
	}()
	go func() {
		defer wg.Done()
		shuttingDown := false
		for !shuttingDown {
			var cur int
			cur, shuttingDown = Get()
			fmt.Printf("当时消费 %d, 行列剩下 %d \n", cur, cnt)
		}
	}()
	time.Sleep(time.Second * 5)
	Shutdown()
	wg.Wait()
  • 别离创立出产者与顾客。
  • 出产10个,每5个歇息1秒。
  • 继续消费。
  • 主程序封闭行列。

输出

出产咯,来消费吧
当时消费 1, 行列剩下 0 
未封闭但空了,等候出产
出产咯,来消费吧
出产咯,来消费吧
当时消费 1, 行列剩下 1 
当时消费 1, 行列剩下 0 
未封闭但空了,等候出产
出产咯,来消费吧
出产咯,来消费吧
出产咯,来消费吧
当时消费 1, 行列剩下 2 
当时消费 1, 行列剩下 1 
当时消费 1, 行列剩下 0 
未封闭但空了,等候出产
出产咯,来消费吧
出产咯,来消费吧
出产咯,来消费吧
出产咯,来消费吧
当时消费 1, 行列剩下 1 
当时消费 1, 行列剩下 2 
当时消费 1, 行列剩下 1 
当时消费 1, 行列剩下 0 
未封闭但空了,等候出产
要封闭咯,我们快消费
封闭咯,也消费完咯
当时消费 0, 行列剩下 0

小结

  • sync.Map 并发安全的Map。
  • sync.Once 只履行一次,适用于配置读取、通道封闭。
  • sync.Cond 操控和谐同享资源。

引证

  • Go sync.Once
  • k8s workerqueue源码