MST

星途 面试题库

面试题:Go语言中RWMutex锁的读锁和写锁在使用上有什么区别?

请阐述Go语言RWMutex锁的读锁和写锁在加锁、解锁操作以及并发访问控制方面的区别,并举例说明在实际应用场景中如何正确使用读锁和写锁以提高程序性能。
15.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

读锁和写锁在加锁、解锁操作以及并发访问控制方面的区别

  1. 加锁操作
    • 读锁(RLock):多个读操作可以同时持有读锁,只要没有写锁被持有。当调用RLock方法时,如果此时没有写锁被持有,读锁立即获取成功,多个读操作可以并行执行。
    • 写锁(Lock):写锁是排他性的,当调用Lock方法时,如果此时有读锁或其他写锁被持有,写锁会阻塞,直到所有读锁和写锁都被释放,才能获取写锁成功。
  2. 解锁操作
    • 读锁(RUnlock):读锁通过RUnlock方法解锁。每一个RLock调用都应该对应一个RUnlock调用,以释放读锁资源。如果读锁解锁次数多于加锁次数,会导致运行时错误。
    • 写锁(Unlock):写锁通过Unlock方法解锁。同样,每一个Lock调用都应该对应一个Unlock调用,否则也会导致运行时错误。
  3. 并发访问控制
    • 读锁:适用于多个协程同时读取共享资源的场景,允许多个读操作并发进行,提高了读操作的并发性能。但是读锁不能与写锁同时存在,因为写操作需要对共享资源进行修改,必须保证数据一致性。
    • 写锁:用于对共享资源进行写操作的场景,写锁会阻止其他读锁和写锁的获取,确保在写操作期间,没有其他协程能够访问共享资源,从而保证数据的一致性。

实际应用场景中正确使用读锁和写锁以提高程序性能的示例

假设我们有一个应用场景,需要在内存中维护一个用户信息的缓存。多个协程可能会频繁读取用户信息,但偶尔也会有协程更新用户信息。

package main

import (
    "fmt"
    "sync"
)

type UserCache struct {
    data map[string]string
    mu   sync.RWMutex
}

// GetUser 获取用户信息
func (uc *UserCache) GetUser(key string) string {
    uc.mu.RLock()
    defer uc.mu.RUnlock()
    return uc.data[key]
}

// SetUser 设置用户信息
func (uc *UserCache) SetUser(key, value string) {
    uc.mu.Lock()
    defer uc.mu.Unlock()
    if uc.data == nil {
        uc.data = make(map[string]string)
    }
    uc.data[key] = value
}

func main() {
    var wg sync.WaitGroup
    uc := UserCache{}

    // 模拟多个读操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("user%d", id)
            value := uc.GetUser(key)
            fmt.Printf("Reader %d got value: %s\n", id, value)
        }(i)
    }

    // 模拟写操作
    wg.Add(1)
    go func() {
        defer wg.Done()
        uc.SetUser("user1", "John Doe")
        fmt.Println("Writer updated user1")
    }()

    wg.Wait()
}

在上述代码中:

  • GetUser方法使用读锁(RLock),因为它只是读取数据,允许多个协程同时调用,提高了读取的并发性能。
  • SetUser方法使用写锁(Lock),因为它会修改共享资源(data map),为了保证数据一致性,需要独占访问,防止其他读或写操作干扰。这样通过合理使用读锁和写锁,既保证了数据一致性,又提高了程序在读写并发场景下的性能。