3.1 ReentrantLock(重入锁)
在并发编程中,有时需要一个锁能够被同一线程多次获取而不会发生死锁,这就是所谓的重入锁。在其他编程语言(如 Java)中,重入锁是一种常见的同步机制。然而,Go 语言没有直接提供类似 Java 中 ReentrantLock 的锁,但我们可以使用 Go 的 sync.Mutex 以及 Goroutine 的特性来实现重入锁。下面我们详细介绍重入锁的概念、使用方法及示例。
3.1.1 什么是ReentrantLock
ReentrantLock 是一种允许同一线程多次获取的锁。每次获取锁后必须对应一个释放锁的操作,只有当所有的获取操作都被释放后,其他线程才能获取该锁。
重入锁有以下特点:
- 可重入性:同一线程可以多次获取同一把锁。
- 计数器:每获取一次锁,计数器增加1;每释放一次锁,计数器减少1。当计数器为0时,锁才真正被释放。
3.1.2 Go语言中的重入锁实现
虽然 Go 语言标准库中没有直接提供重入锁,但我们可以通过组合 sync.Mutex 和 Goroutine 的特性来实现。以下是一个简单的重入锁实现:
package main
import (
"fmt"
"sync"
)
type ReentrantLock struct {
mu sync.Mutex
owner int64
recursion int
}
func (rl *ReentrantLock) Lock() {
id := getGID()
if rl.owner == id {
rl.recursion++
return
}
rl.mu.Lock()
rl.owner = id
rl.recursion = 1
}
func (rl *ReentrantLock) Unlock() {
if rl.owner != getGID() {
panic("unlock of unowned lock")
}
rl.recursion--
if rl.recursion == 0 {
rl.owner = -1
rl.mu.Unlock()
}
}
func getGID() int64 {
// 使用反射获取当前 Goroutine ID
var buf [64]byte
n := runtime.Stack(buf[:], false)
idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
id, err := strconv.ParseInt(idField, 10, 64)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
func main() {
rl := &ReentrantLock{}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
rl.Lock()
fmt.Println("First lock acquired")
rl.Lock()
fmt.Println("Second lock acquired")
rl.Unlock()
fmt.Println("First unlock")
rl.Unlock()
fmt.Println("Second unlock")
}()
wg.Wait()
}
在这个示例中,ReentrantLock 通过 owner 字段记录当前持有锁的 Goroutine ID,通过 recursion 记录重入次数。Lock 方法根据当前 Goroutine ID 判断是否为重入锁操作,如果是则增加重入次数,否则获取锁。Unlock 方法根据重入次数判断是否完全释放锁。
3.1.3 重入锁的应用场景
重入锁适用于以下场景:
- 递归操作:在递归操作中,需要多次获取同一把锁。
- 复杂同步逻辑:在复杂的同步逻辑中,同一线程可能需要多次获取锁以完成任务。
- 避免死锁:重入锁可以避免在同一线程中多次获取锁而导致的死锁问题。
3.1.4 示例代码
以下是一个使用重入锁保护共享资源的示例:
package main
import (
"fmt"
"sync"
"time"
)
type SharedResource struct {
lock ReentrantLock
data int
}
func (sr *SharedResource) Increment() {
sr.lock.Lock()
defer sr.lock.Unlock()
sr.data++
}
func main() {
sr := &SharedResource{}
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
sr.Increment()
time.Sleep(10 * time.Millisecond)
}
}()
}
wg.Wait()
fmt.Printf("Final data value: %d\n", sr.data)
}
在这个示例中,我们定义了一个 SharedResource 结构体,使用重入锁保护对 data 字段的访问,确保其在并发情况下的安全性。
结论
重入锁是一种允许同一线程多次获取的锁,可以在递归操作和复杂同步逻辑中避免死锁问题。虽然 Go 语言没有直接提供重入锁,但通过组合 sync.Mutex 和 Goroutine 的特性,我们可以实现类似的功能。在实际应用中,需要根据具体需求选择合适的同步原语,以达到最佳的性能和安全性。在接下来的章节中,我们将继续探讨其他同步原语和并发编程技巧,帮助您更好地掌握 Go 的并发编程。