介绍

在这个包下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标准库就介绍到这吧,相信大家工作中也经常用到。赶紧学起来吧。


欢迎关注,学习不迷路!