Go并发系列:3扩展同步原语-3.1 ReentrantLock(重入锁)

217 阅读3分钟

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 重入锁的应用场景

重入锁适用于以下场景:

  1. 递归操作:在递归操作中,需要多次获取同一把锁。
  2. 复杂同步逻辑:在复杂的同步逻辑中,同一线程可能需要多次获取锁以完成任务。
  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 的并发编程。