go 读写锁

在 Go 语言中,读写锁RWMutex)是一种同步机制,用于控制对共享资源的并发访问。读写锁允许多个读操作同时进行,但写操作是独占的(即写操作时不允许其他读或写操作)。这种机制非常适合读多写少的场景。

读写锁的核心概念

  1. 读锁(RLock)
    • 多个 goroutine 可以同时持有读锁。
    • 读锁的目的是允许多个 goroutine 同时读取共享资源,而不会互相阻塞。
    • 如果写锁被持有,读锁会被阻塞,直到写锁释放。
  2. 写锁(Lock)
    • 写锁是独占的,同一时间只能有一个 goroutine 持有写锁。
    • 写锁的目的是确保在写入共享资源时,没有其他 goroutine 在读或写。
    • 如果读锁或写锁被持有,写锁会被阻塞,直到所有锁释放。
  3. 解锁(RUnlock 和 Unlock)
    • 读锁通过 RUnlock 释放。
    • 写锁通过 Unlock 释放。

读写锁的使用场景

  • 读多写少:当你的程序需要频繁读取共享资源,但很少修改时,读写锁可以显著提高性能。
  • 数据一致性:当需要确保写操作独占资源时,读写锁可以避免数据竞争。

读写锁的实例

以下是一个简单的示例,展示了如何使用 sync.RWMutex 来保护共享资源。

package main

import (
	"fmt"
	"sync"
	"time"
)

// 共享资源
var (
	sharedData map[string]string
	rwMutex    sync.RWMutex
)

func init() {
	sharedData = make(map[string]string)
	sharedData["name"] = "Go"
}

// 读操作
func readData(id int, wg *sync.WaitGroup) {
	defer wg.Done()

	rwMutex.RLock() // 加读锁
	defer rwMutex.RUnlock() // 释放读锁

	// 模拟读取操作
	fmt.Printf("Goroutine %d is reading data: %s\n", id, sharedData["name"])
	time.Sleep(100 * time.Millisecond) // 模拟耗时操作
}

// 写操作
func writeData(id int, wg *sync.WaitGroup) {
	defer wg.Done()

	rwMutex.Lock() // 加写锁
	defer rwMutex.Unlock() // 释放写锁

	// 模拟写入操作
	fmt.Printf("Goroutine %d is writing data\n", id)
	sharedData["name"] = fmt.Sprintf("Go-%d", id)
	time.Sleep(200 * time.Millisecond) // 模拟耗时操作
}

func main() {
	var wg sync.WaitGroup

	// 启动多个读操作
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go readData(i, &wg)
	}

	// 启动一个写操作
	wg.Add(1)
	go writeData(1, &wg)

	// 再启动多个读操作
	for i := 6; i <= 10; i++ {
		wg.Add(1)
		go readData(i, &wg)
	}

	// 等待所有 goroutine 完成
	wg.Wait()

	// 打印最终结果
	fmt.Println("Final data:", sharedData["name"])
}

代码解析

  1. 共享资源
    • sharedData 是一个共享的 map,多个 goroutine 会同时读取或写入它。
  2. 读操作
    • 使用 rwMutex.RLock() 加读锁,允许多个 goroutine 同时读取。
    • 使用 rwMutex.RUnlock() 释放读锁。
  3. 写操作
    • 使用 rwMutex.Lock() 加写锁,确保写操作独占资源。
    • 使用 rwMutex.Unlock() 释放写锁。
  4. 并发执行
    • 启动了多个读操作和一个写操作,读操作可以并发执行,但写操作会阻塞其他读和写操作。
  5. 输出结果
    • 由于写操作是独占的,它会阻塞其他操作,直到完成。
    • 最终输出的 sharedData["name"] 是写操作修改后的值。

运行结果示例

plaintext
复制
Goroutine 1 is reading data: Go
Goroutine 2 is reading data: Go
Goroutine 3 is reading data: Go
Goroutine 4 is reading data: Go
Goroutine 5 is reading data: Go
Goroutine 1 is writing data
Goroutine 6 is reading data: Go-1
Goroutine 7 is reading data: Go-1
Goroutine 8 is reading data: Go-1
Goroutine 9 is reading data: Go-1
Goroutine 10 is reading data: Go-1
Final data: Go-1

注意事项

  1. 避免死锁
    • 确保每次加锁后都有对应的解锁操作。
    • 使用 defer 可以避免忘记解锁。
  2. 性能优化
    • 读写锁适合读多写少的场景。如果写操作频繁,可能会降低性能。
  3. 锁的粒度
    • 尽量减少锁的持有时间,避免在锁内执行耗时操作。

通过这个示例,你可以理解 Go 中读写锁的基本用法和适用场景。读写锁是 Go 并发编程中非常重要的工具,合理使用可以显著提高程序的并发性能。