锁
# 概述
当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)显然这无法让人容忍,那我们该如何解决这个问题?
经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。
map类型是非线程安全的,当并行访问一个共享的map类型的数据,map数据将会出错。
在Go语言中这种锁的机制是通过sync包中Mutex来实现的。sync来源“synchronized”一词,这意味着线程将有序的对同一变量进行访问。
# 互斥锁
sync.Mutex 是一个互斥锁,它的作用是守护在临界区入口来确保同一时间只能有一个线程进入临界区。
假设info是一个需要上锁的放在共享内存中的变量。通过包含Mutex来实现的一个典型例子如下:
import "sync"
type Info struct {
mu sync.Mutex
Str string
}
2
3
4
5
6
如果一个函数想要改变这个变量可以这样写:
func update(info *Info) {
info.mu.Lock()
info.Str = ""
info.mu.Unlock()
}
2
3
4
5
还有一个很有用的例子是通过Mutex来实现一个可以上锁的共享缓冲器:
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
2
3
4
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock()
x = + 1
lock.Unlock()
}
wg.Done()
}
2
3
4
5
6
7
8
9
10
11
12
13
# 读写互斥锁
在sync包中还有一个RWMutex锁:他能通过RLock()来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用Lock()将和普通的Mutex作用相同。
var (
lock sync.Mutex
rwlock sync.RWMutex
)
func read() {
rwlock.RLock()
x := + 1
rwlock.RUnLock()
}
2
3
4
5
6
7
8
9
10
# Sync.Once
包中还有一个方便的Once类型变量的方法once.Do(call),这个方法确保被调用函数只能被调用一次。 相对简单的情况下,通过使用sync包可以解决同一时间只能一个线程访问变量或map类型数据的问题。如果这种方式导致程序明显变慢或者引起其他问题,我们要重新思考来通过goroutines和Channels 来解决问题,这是在Go语言中所提倡用来实现并发的技术。
# 实现可重入锁
type RecursiveMutex struct {
sync.Mutex
owner int64 //当前持有锁的goroutine id
recursion int32 //重入次数
}
func (m *RecursiveMutex) Lock() {
gid := goid.Get()
if atomic.LoadInt64(&m.owner) == gid {
m.recursion++
return
}
m.Mutex.Lock()
atomic.StoreInt64(&m.owner, gid)
m.recursion = 1
}
func (m *RecursiveMutex) Unlock() {
gid := goid.Get()
if atomic.LoadInt64(&m.owner) != gid {
panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
}
m.recursion--
if m.recursion != 0 {
return
}
atomic.StoreInt64(&m.owner, -1)
m.Mutex.Unlock()
}
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
# TryLock
在某些情况下,我们希望在获取锁失败时,并不想阻塞等待,而是希望进入其它逻辑。这时候,就需要TryLock方法,此方法是在go1.18版本,被官方加入。当调用 TryLock()时,该函数仅返回true或者false,代表是否加锁成功。