文章目录
- 1. 互斥锁
- 2. 读写互斥锁
- 3. sync.Once
- 4. sync.Map
- 5. 定时器
在 go 代码中可能会存着多个 goroutine 同时操作一个资源(临界区),这种情况会发生竞态。例如:商场更衣间。
1. 互斥锁
- 互斥锁是一种常用的控制共享资源访问的方法,它能够保证只有一个 goroutine 访问共享资源。
- 互斥锁作用:同一时间有且仅有一个 goroutine 进入临界区,其他 goroutine 则在等待锁,等互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 都在等待一个锁时,唤醒机制是随机的。
示例:
package main
import (
"fmt"
"sync"
)
//全局变量
var x int64
//计数器
var sw sync.WaitGroup
//互斥锁
var lock sync.Mutex
func add() {
defer sw.Done()
for i := 0; i < 5000; i++ {
lock.Lock() //加锁
x++
lock.Unlock() //解锁
}
}
func main() {
sw.Add(2)
go add()
go add()
sw.Wait()
fmt.Println(x)
}
//输出结果如下
10000
2. 读写互斥锁
- 互斥锁是完全互斥的,但是很多的实际场景,读多写少,当并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景使用读写锁是更好的一种选择,可以提高性能。
- 读写锁分两种:读锁和写锁。当一个 goroutine 获取读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
示例:
package main
import (
"fmt"
"sync"
"time"
)
//锁执行时间测试
var x int64
//计数器
var sw sync.WaitGroup
//互斥锁
var lock sync.Mutex
//读写锁
var rwLock sync.RWMutex
//读函数
func read() {
defer sw.Done()
rwLock.RLock() //读锁加锁
//读取时间1毫秒
time.Sleep(time.Microsecond)
rwLock.RUnlock() //读锁解锁
}
//写函数
func write() {
defer sw.Done()
rwLock.Lock() //写锁加锁
//写入5毫秒
x++
time.Sleep(time.Millisecond * 5)
rwLock.Unlock() //写锁解锁
}
func main() {
//启动时间戳
start := time.Now()
//写入10次
for i := 0; i < 10; i++ {
sw.Add(1)
go write()
}
//读取1000次
for i := 0; i < 1000; i++ {
sw.Add(1)
go read()
}
sw.Wait()
end := time.Now()
fmt.Printf("使用时间: %v", end.Sub(start))
}
//输出结果如下
使用时间: 165.518ms
3. sync.Once
- sync.Once 用于延迟一个开销很大的初始化操作,到真正用到它的时候再执行
- 例如,定义了一个 init 初始化函数,程序启动的时候会被自动加载,无论是否用到都会加载,这样程序就会增加程序的启动延时。
示例:
package main
//龙类
type Dragon struct {
name string
property string
}
//加载龙类
func loadDragon(n, p string) Dragon {
dragon := Dragon{
name: n,
property: p,
}
return dragon
}
var cards map[string]Dragon
//卡牌
func loadCards() {
cards = map[string]Dragon{
"red": loadDragon("红龙", "火"),
"bule": loadDragon("蓝龙", "冰"),
"white": loadDragon("白龙", "光"),
"black": loadDragon("黑龙", "暗"),
}
}
//被多个goroutine调用时不是并发安全的
func card(name string) Dragon {
if cards == nil {
loadCards()
}
return cards[name]
}
多个goroutine并发调动card函数不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上,自由重排访问内存的顺序。loadCards函数可能会被重排。
func loadCards() {
cards =make(map[string]Dragon)
cards["red"]=loadDragon("红龙","火")
cards["blue"]=loadDragon("蓝龙","冰")
cards["white"]=loadDragon("白龙","光")
cards["black"]=loadDragon("黑龙","暗")
}
- 这种情况下会出现即使判断了 cards 不是 nil 也不意味着初始化完成。
- 考虑到这种情况,解决的办法就是添加互斥锁,保证初始化 cards 的时候不会被其他 goroutine 操作,单这样做又会引发性能问题。
- 于是 Go 提供了一种解决方案,sync.Once
- sync.Once只有一个Do方法,示例:
//定义sync.Once
var onlyOne sync.Once
//被多个goroutine调用时并不是并发安全的
func card(name string) Dragon {
if cards == nil {
onlyOne.Do(loadCards)
}
return cards[name]
}
- 使用sync.Once调用带参函数
package main
import (
"fmt"
"sync"
)
//sync.Once
var onlyOne sync.Once
func test(x int) {
fmt.Println(x)
}
//闭包
func closer(x int) func() {
return func() {
test(x)
}
}
func main() {
//函数变量
t := closer(10)
onlyOne.Do(t)
}
//输出结果如下
10
4. sync.Map
package main
import (
"fmt"
"strconv"
"sync"
)
var m = make(map[string]int)
//设置map
func set(key string, value int) {
m[key] = value
}
//获取map值
func get(key string) int {
return m[key]
}
func main() {
sw := sync.WaitGroup{}
for i := 0; i < 10; i++ {
sw.Add(1)
//匿名函数
go func(n int) {
key := strconv.Itoa(n) //整形转字符串
set(key, n)
fmt.Printf("k:%s,v:%v\n", key, get(key))
sw.Done()
}(i)
}
sw.Wait()
}
运行结果如下
- 针对这种场景,就需要给map加锁保证并发安全,Go提供了一种开箱即用的安全map,就是
sync.Map
-
sync.Map
不需要像内置map一样需要初始化,直接就能使用 - 它的内部定义了,
Store,Load,LoadOrStore,Delete,Range
等操作方法
package main
import (
"fmt"
"strconv"
"sync"
)
var m = sync.Map{}
func main() {
sw := sync.WaitGroup{}
for i := 0; i < 10; i++ {
sw.Add(1)
//匿名函数
go func(n int) {
key := strconv.Itoa(n) //整形转字符串
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("key:%s,value:%v\n", key, value)
sw.Done()
}(i)
}
sw.Wait()
}
//输出结果如下
key:9,value:9
key:8,value:8
key:1,value:1
key:0,value:0
key:3,value:3
key:6,value:6
key:4,value:4
key:2,value:2
key:5,value:5
key:7,value:7
5. 定时器
指定间隔时间执行任务
package main
import (
"fmt"
"time"
)
//定时器
func tickDemo() {
ticker := time.Tick(time.Second) //定义1秒间隔定时器
for i := range ticker {
fmt.Println(i) //每秒都会执行任务
}
}
func main() {
tickDemo()
}
//输出结果如下
2022-04-27 17:48:22.2357747 +0800 CST m=+1.012006201
2022-04-27 17:48:23.2406837 +0800 CST m=+2.016915201
2022-04-27 17:48:24.2391351 +0800 CST m=+3.015366601
...