临界区、互斥锁、读写锁,sync 包下锁的使用(不推荐,推荐使用信道)以及并发安全的 Map 类型
临界区的概念
多个 SubGoroutine 同时操作一个变量,竞争同一个资源,发生竞态问题(数据竞态)
当多个 SubGoroutine 并发时,多个 SubGoroutine 不应该同时访问那些修改共享资源的代码,这些修改共享资源的代码,就叫做临界区
如下代码中,globalX 的变量每次输出都是不同的值,得到的结果并不是 1000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func taskForOneVar(wg *sync.WaitGroup) {
globalX++ // 这段代码叫做临界区
wg.Done()
}
func main() {
var wg sync.WaitGroup
for i := 1; i < 1000; i++ {
wg.Add(1)
go taskForOneVar(&wg)
}
wg.Wait()
fmt.Println(globalX)
}
sync.Mutex(Golang 不推崇,推荐使用信道)
互斥锁,多 Goroutine 情况下,操作同一个变量,竞争同一个资源,发生竞态问题,可能由于并发,从而导致结果不准确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义一个变量,多个 Goroutine 对这个变量进行修改
var globalX int = 1
func taskForOneVar(lock *sync.Mutex, wg *sync.WaitGroup) {
// 为什么让 wg.Done() 注册并延迟调用?为了防止代码运行异常,导致该 SubGoroutine 没有返回 wg.Done(),继而导致 MainGoroutine 一直在 wg.Wait() 处等待
defer wg.Done()
// 上 Mutex 互斥锁
lock.Lock()
globalX++ // 这段代码叫做临界区
// 解 Mutex 互斥锁
lock.Unlock()
}
func main() {
// Mutex 是一个结构体(值类型),有加锁(Locker)、解锁(Unlocker)方法,实现了 Locker 接口
var lockCode sync.Mutex
// 如果不用 wg.Wait() 等所有 taskForOneVar() 函数执行完,中途打印出的数字,是小于 1000 的,等待所有 Goroutine 执行完毕才结果是 1000
var wg sync.WaitGroup
for i := 1; i < 1000; i++ {
wg.Add(1)
// sync.WaitGroup 和 sync.Mutex 都是 struct 类型,也就意味着需要传入地址的指针进去,否则是 Copy 值传递的方式传入参数
go taskForOneVar(&lockCode, &wg)
}
wg.Wait()
fmt.Println(globalX)
}
sync.Rwmutex(Golang 不推崇,推荐使用信道)
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 全局共享变量,用来测试读写锁的效率
var globalY int = 1
// 定义一个互斥锁
var lock sync.Mutex
// 定义一个读写锁
var rwlock sync.RWMutex
func writeY(wg *sync.WaitGroup) {
defer wg.Done()
rwlock.Lock()
globalY += 10
rwlock.Unlock()
}
func readY(wg *sync.WaitGroup) {
defer wg.Done()
// 使用互斥锁(访问同一个共享变量,即使读也是要竞争的)
lock.Lock()
lock.Unlock()
// 使用读锁,访问同一个共享变量,读锁是可以避免竞争的,那么理论上这段代码更快些
// rwlock.RLock()
// rwlock.RUnlock()
}
func main() {
startTime := time.Now()
var wg sync.WaitGroup
// 设置 CPU 数,为逻辑 CPU 的数量 - 1
runtime.GOMAXPROCS(runtime.NumCPU() - 1)
for i := 0; i < 10; i++ {
wg.Add(1)
go writeY(&wg)
}
for i := 0; i < 10000000; i++ {
wg.Add(1)
go readY(&wg)
}
wg.Wait()
endTime := time.Now()
// 计算出从 startTime - endTime 度过的时间,以此衡量读锁和互斥锁的效率
fmt.Println("用时", endTime.Sub(startTime))
}
另外的一个例子,运行的结果如图,可以看到加了读锁下的包裹的代码,各个 Goroutine 是可以同时访问并执行的,反之写锁包裹的代码不可以,是串行的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var count int
var mutex sync.RWMutex
func write(n int) {
rand.Seed(time.Now().UnixNano())
fmt.Printf("写 goroutine %d 正在写数据...\n", n)
mutex.Lock()
num := rand.Intn(500)
count = num
fmt.Printf("写 goroutine %d 写数据结束,写入新值 %d\n", n, num)
mutex.Unlock()
}
func read(n int) {
mutex.RLock()
fmt.Printf("读 goroutine %d 正在读取数据...\n", n)
num := count
fmt.Printf("读 goroutine %d 读取数据结束,读到 %d\n", n, num)
mutex.RUnlock()
}
func main() {
for i := 0; i < 10; i++ {
go read(i + 1)
}
for i := 100; i < 110; i++ {
go write(i + 1)
}
time.Sleep(time.Second * 5)
}
当我们把读锁换成写锁,输出结果如图,就更方便你理解了
sync.Once(多 Goroutine 并发可控,只执行一次)
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的 sync 包中提供了一个针对只执行一次场景的解决方案 sync.Once,另外,sync.Once 只有一个 Do 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 小美去淘宝购物,只加载一次这个账号
type Woman struct {
name string
}
var (
once sync.Once
shoppingUser *Woman
)
func getUser() *Woman {
// once.Do 包裹的代码,无论调用多少次,只执行一次
once.Do(func() {
shoppingUser = new(Woman)
shoppingUser.name = "小美"
fmt.Println("Load User 小美.....")
})
// 这行代码会一直执行
fmt.Println("小美 Shopping")
return shoppingUser
}
func main() {
for i := 0; i < 3; i++ {
_ = getUser()
}
}
sync.Once 其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。
这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次
源码的实现的原理需要到这里看下,sync.once 详解
我们其实可以将上述 for 中的代码,使用多个 Goroutine 去并发执行,用 sync.WaitGroup 去控制我们的并发协程,结果仍然是一样的
小美有三头六臂,影分身,可以派出去分别购物,但是她们的身份仍是小美
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 小美去淘宝购物,只加载一次这个账号
type Woman struct {
name string
}
var (
once sync.Once
shoppingUser *Woman
)
func actionOfUser() *Woman {
// once.Do 包裹的代码,无论调用多少次,只执行一次
once.Do(func() {
shoppingUser = new(Woman)
shoppingUser.name = "小美"
fmt.Println("Load User 小美.....")
})
// 这行代码会一直执行
fmt.Println("小美 Shopping")
return shoppingUser
}
func main() {
var wg sync.WaitGroup
// 小美有三头六臂,影分身,可以派出去分别购物,但是她们的身份仍是小美,不需要重新加载(放在 once.Do 中的代码)
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
actionOfUser()
wg.Done()
}()
}
wg.Wait()
}
这样就保证了协程并发安全
sync.map 并发安全的字典类型
下面代码并没有使用 Goroutine 哦,初步使用下 rand.Seed() && rand.Intn() 生成随机数,fmt.Sprintf() 拼接,以及 strconv.Itoa() 强制转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var mapTest map[string]interface{}
func main() {
mapTest = make(map[string]interface{})
mapTest["age"] = 16
mapTest["name"] = "ethan"
mapTest["hobby"] = []string{"sing", "rap", "basketball"}
// 自定义函数,通过函数赋值,通过函数获取值
setValue("address", "earth")
addr := getValue("address")
fmt.Println(addr)
// time.Now().UnixNano() 纳秒,time.Now().Unix() 秒
fmt.Println(time.Now().UnixNano())
fmt.Println(time.Now().Unix())
// 利用 timestamp 种下 Seed ,用来生成随机数
rand.Seed(time.Now().UnixNano())
// 循环向这个 mapTest 中赋值
for i := 0; i < 11; i++ {
// strconv.Itoa(int64) 可以用来将 int64 强制转换成 string 类型,rand.Intn(100) 生成 0-100 的随机数
// setValue(strconv.Itoa(i), rand.Intn(100))
// 使用 Sprintf 拼接 string 和 int 类型,不建议使用 stu := "student" + strconv.Itoa(i),如下代码更方便格式化
stu := fmt.Sprintf("student%03d", i)
setValue(stu, rand.Intn(100))
}
fmt.Println(mapTest)
}
func setValue(key string, value interface{}) {
mapTest[key] = value
}
func getValue(key string) interface{} {
return mapTest[key]
}
通过 使用 Sprintf 拼接 string 和 int 类型,我们可以使用 %03d 达到一些效果 001-010
现在,先看下下面这个 Goroutine 例子
1
2
3
4
5
6
7
8
func main() {
for i := 0; i < 100; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second * 3)
}
观察打印的结果,为什么?因为在 go func() 这个函数中,i 属于外部变量(也就是匿名+ 闭包函数),所以 Goroutine 真正执行的时刻,i 的值已经不是 go func() 调用时刻的值,每个 Goroutine 拉起程序并执行的速度是不一样的,执行到 fmt.Println(i) 时,i 的值早已经不归 for 循环内控制,因为不是串行
上述内容中,只是 int 类型的并发,如果时 map 类型的数据在多个 Goroutine 中并发操作,会报错 fatal error: concurrent map writes
这里是 map 的例子,并发不安全的 Map 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var stuList map[string]interface{} = make(map[string]interface{})
func main() {
rand.Seed(time.Now().UnixNano())
for i := 1; i < 3; i++ {
go func(num int) {
key := fmt.Sprintf("students%02d", num)
stuList[key] = rand.Intn(100)
}(i)
}
time.Sleep(time.Second * 3)
fmt.Println(stuList)
}
这里是 sync.map 的例子,并发安全的 Map 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 多个 Goroutine 并发访问/修改同一个 Map 类型数据时,会 fatal
// 定义一个并发安全的 Map
var safeMap sync.Map
func main() {
rand.Seed(time.Now().UnixNano())
for i := 1; i < 11; i++ {
go func(num int) {
key := fmt.Sprintf("studen%02d", num)
// 用 Store 方法定义 Key:Value
safeMap.Store(key, rand.Intn(100))
// 用 Load 方法根据 Key 获取 Value,一共有两个返回值,ok 代表是否有这个 Key
score, ok := safeMap.Load(key)
fmt.Println(score, ok)
}(i)
}
time.Sleep(time.Second * 1)
}